Selaa lähdekoodia

Add AGI zip library and improve image loading UX

Toby Chui 1 viikko sitten
vanhempi
commit
ca9cb34dd7
4 muutettua tiedostoa jossa 1200 lisäystä ja 112 poistoa
  1. 1151 0
      src/mod/agi/agi.zip.go
  2. 1 1
      src/web/Photo/index.html
  3. 36 111
      src/web/Photo/photo.js
  4. 12 0
      src/web/desktop.html

+ 1151 - 0
src/mod/agi/agi.zip.go

@@ -0,0 +1,1151 @@
+package agi
+
+import (
+	"archive/zip"
+	"encoding/json"
+	"errors"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/mholt/archiver/v3"
+	"github.com/robertkrimen/otto"
+	"imuslab.com/arozos/mod/agi/static"
+	"imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/filesystem/arozfs"
+)
+
+/*
+	AGI Zip File View or Extract Library
+
+	This library provide agi API for apps that want to view or extract agi zip files.
+
+	Author: tobychui
+*/
+
+func (g *Gateway) ZipLibRegister() {
+	err := g.RegisterLib("ziplib", g.injectZipFileLibFunctions)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPayload) {
+	vm := payload.VM
+	u := payload.User
+	scriptFsh := payload.ScriptFsh
+
+	// extractZipFile(sourceVpath, destVpath) => Extract zip file to destination
+	vm.Set("_ziplib_extractZipFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Rewrite paths if relative
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		// Check permissions
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		// Get real paths
+		_, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Extract using archiver
+		z := archiver.Zip{}
+		err = z.Unarchive(srcRpath, destRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Update ownership
+		destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
+			if err == nil && !info.IsDir() {
+				vp, _ := static.RealpathToVirtualpath(destFsh, path, u)
+				u.SetOwnerOfFile(destFsh, vp)
+			}
+			return nil
+		})
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// createZipFile(sourceVpaths, outputVpath) => Create zip file from array of sources
+	vm.Set("_ziplib_createZipFile", func(call otto.FunctionCall) otto.Value {
+		sourcesObj, err := call.Argument(0).Export()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		outputVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Convert sources to string array
+		var sources []string
+		switch v := sourcesObj.(type) {
+		case []interface{}:
+			for _, s := range v {
+				if str, ok := s.(string); ok {
+					sources = append(sources, static.RelativeVpathRewrite(scriptFsh, str, vm, u))
+				}
+			}
+		case string:
+			sources = append(sources, static.RelativeVpathRewrite(scriptFsh, v, vm, u))
+		default:
+			g.RaiseError(errors.New("invalid source format"))
+			return otto.FalseValue()
+		}
+
+		outputVpath = static.RelativeVpathRewrite(scriptFsh, outputVpath, vm, u)
+
+		// Check permissions
+		for _, src := range sources {
+			if !u.CanRead(src) {
+				panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+src))
+			}
+		}
+		if !u.CanWrite(outputVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+outputVpath))
+		}
+
+		// Get real paths
+		var sourceRpaths []string
+		var sourceFshs []*filesystem.FileSystemHandler
+		for _, src := range sources {
+			fsh, rpath, err := static.VirtualPathToRealPath(src, u)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			sourceFshs = append(sourceFshs, fsh)
+			sourceRpaths = append(sourceRpaths, rpath)
+		}
+
+		outputFsh, outputRpath, err := static.VirtualPathToRealPath(outputVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Create zip file
+		err = filesystem.ArozZipFile(sourceFshs, sourceRpaths, outputFsh, outputRpath, false)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Set ownership
+		u.SetOwnerOfFile(outputFsh, outputVpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// createTarFile(sourceVpaths, outputVpath) => Create tar file
+	vm.Set("_ziplib_createTarFile", func(call otto.FunctionCall) otto.Value {
+		sourcesObj, err := call.Argument(0).Export()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		outputVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		var sources []string
+		switch v := sourcesObj.(type) {
+		case []interface{}:
+			for _, s := range v {
+				if str, ok := s.(string); ok {
+					sources = append(sources, static.RelativeVpathRewrite(scriptFsh, str, vm, u))
+				}
+			}
+		case string:
+			sources = append(sources, static.RelativeVpathRewrite(scriptFsh, v, vm, u))
+		}
+
+		outputVpath = static.RelativeVpathRewrite(scriptFsh, outputVpath, vm, u)
+
+		for _, src := range sources {
+			if !u.CanRead(src) {
+				panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+src))
+			}
+		}
+		if !u.CanWrite(outputVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+outputVpath))
+		}
+
+		var sourceRpaths []string
+		for _, src := range sources {
+			_, rpath, err := static.VirtualPathToRealPath(src, u)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			sourceRpaths = append(sourceRpaths, rpath)
+		}
+
+		outputFsh, outputRpath, err := static.VirtualPathToRealPath(outputVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		tar := archiver.Tar{}
+		err = tar.Archive(sourceRpaths, outputRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		u.SetOwnerOfFile(outputFsh, outputVpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// extractTarFile(sourceVpath, destVpath) => Extract tar file
+	vm.Set("_ziplib_extractTarFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		_, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		tar := archiver.Tar{}
+		err = tar.Unarchive(srcRpath, destRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Update ownership
+		destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
+			if err == nil && !info.IsDir() {
+				vp, _ := static.RealpathToVirtualpath(destFsh, path, u)
+				u.SetOwnerOfFile(destFsh, vp)
+			}
+			return nil
+		})
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// createTarGzFile(sourceVpaths, outputVpath) => Create tar.gz file
+	vm.Set("_ziplib_createTarGzFile", func(call otto.FunctionCall) otto.Value {
+		sourcesObj, err := call.Argument(0).Export()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		outputVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		var sources []string
+		switch v := sourcesObj.(type) {
+		case []interface{}:
+			for _, s := range v {
+				if str, ok := s.(string); ok {
+					sources = append(sources, static.RelativeVpathRewrite(scriptFsh, str, vm, u))
+				}
+			}
+		case string:
+			sources = append(sources, static.RelativeVpathRewrite(scriptFsh, v, vm, u))
+		}
+
+		outputVpath = static.RelativeVpathRewrite(scriptFsh, outputVpath, vm, u)
+
+		for _, src := range sources {
+			if !u.CanRead(src) {
+				panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+src))
+			}
+		}
+		if !u.CanWrite(outputVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+outputVpath))
+		}
+
+		var sourceRpaths []string
+		for _, src := range sources {
+			_, rpath, err := static.VirtualPathToRealPath(src, u)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			sourceRpaths = append(sourceRpaths, rpath)
+		}
+
+		outputFsh, outputRpath, err := static.VirtualPathToRealPath(outputVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		tgz := archiver.TarGz{}
+		err = tgz.Archive(sourceRpaths, outputRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		u.SetOwnerOfFile(outputFsh, outputVpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// extractTarGzFile(sourceVpath, destVpath) => Extract tar.gz file
+	vm.Set("_ziplib_extractTarGzFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		_, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		tgz := archiver.TarGz{}
+		err = tgz.Unarchive(srcRpath, destRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Update ownership
+		destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
+			if err == nil && !info.IsDir() {
+				vp, _ := static.RealpathToVirtualpath(destFsh, path, u)
+				u.SetOwnerOfFile(destFsh, vp)
+			}
+			return nil
+		})
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// createGzFile(sourceVpath, outputVpath) => Create gz file (single file compression)
+	vm.Set("_ziplib_createGzFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		outputVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		outputVpath = static.RelativeVpathRewrite(scriptFsh, outputVpath, vm, u)
+
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(outputVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+outputVpath))
+		}
+
+		srcFsh, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		outputFsh, outputRpath, err := static.VirtualPathToRealPath(outputVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Compress using Gz - open source file and create output file
+		srcFile, err := srcFsh.FileSystemAbstraction.ReadStream(srcRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		defer srcFile.Close()
+
+		outFile, err := outputFsh.FileSystemAbstraction.Create(outputRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		defer outFile.Close()
+
+		gz := archiver.Gz{}
+		err = gz.Compress(srcFile, outFile)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		u.SetOwnerOfFile(outputFsh, outputVpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// extractGzFile(sourceVpath, destVpath) => Extract gz file
+	vm.Set("_ziplib_extractGzFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		srcFsh, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Decompress using Gz - open source file and create output file
+		srcFile, err := srcFsh.FileSystemAbstraction.ReadStream(srcRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		defer srcFile.Close()
+
+		outFile, err := destFsh.FileSystemAbstraction.Create(destRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		defer outFile.Close()
+
+		gz := archiver.Gz{}
+		err = gz.Decompress(srcFile, outFile)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		u.SetOwnerOfFile(destFsh, destVpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// isValidZipFile(vpath) => Check if file is a valid archive (zip, tar, tar.gz, gz, etc.)
+	vm.Set("_ziplib_isValidZipFile", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
+
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+vpath))
+		}
+
+		_, rpath, err := static.VirtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Try to detect format using archiver library
+		_, err = archiver.ByExtension(rpath)
+		if err != nil {
+			// Try by header
+			f, err := os.Open(rpath)
+			if err != nil {
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+			_, err = archiver.ByHeader(f)
+			f.Close()
+			if err != nil {
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+		}
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// listZipFileContents(vpath) => List contents of zip in json tree structure
+	vm.Set("_ziplib_listZipFileContents", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
+
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+vpath))
+		}
+
+		_, rpath, err := static.VirtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		r, err := zip.OpenReader(rpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		defer r.Close()
+
+		// Build tree structure
+		type Node struct {
+			Name     string           `json:"name"`
+			IsDir    bool             `json:"isDir"`
+			Size     int64            `json:"size"`
+			Children map[string]*Node `json:"children,omitempty"`
+		}
+
+		root := &Node{
+			Name:     "/",
+			IsDir:    true,
+			Children: make(map[string]*Node),
+		}
+
+		for _, f := range r.File {
+			parts := strings.Split(filepath.ToSlash(f.Name), "/")
+			current := root
+
+			for i, part := range parts {
+				if part == "" {
+					continue
+				}
+
+				isLast := i == len(parts)-1
+				if current.Children == nil {
+					current.Children = make(map[string]*Node)
+				}
+
+				if _, exists := current.Children[part]; !exists {
+					current.Children[part] = &Node{
+						Name:  part,
+						IsDir: !isLast || f.FileInfo().IsDir(),
+					}
+					if isLast && !f.FileInfo().IsDir() {
+						current.Children[part].Size = int64(f.UncompressedSize64)
+					}
+				}
+				current = current.Children[part]
+			}
+		}
+
+		jsonData, err := json.Marshal(root)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		reply, _ := vm.ToValue(string(jsonData))
+		return reply
+	})
+
+	// listZipFileDir(zipVpath, dirPath) => List contents of specific directory in zip
+	vm.Set("_ziplib_listZipFileDir", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		dirPath, err := call.Argument(1).ToString()
+		if err != nil {
+			// Default to root if not provided
+			dirPath = ""
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		// Open zip file
+		r, err := zip.OpenReader(zipRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		defer r.Close()
+
+		// Normalize the directory path
+		dirPath = filepath.ToSlash(strings.TrimPrefix(dirPath, "/"))
+		if dirPath != "" && !strings.HasSuffix(dirPath, "/") {
+			dirPath = dirPath + "/"
+		}
+
+		// Check if directory exists and collect immediate children
+		var filelist []string
+		dirExists := dirPath == "" // root always exists
+		seenItems := make(map[string]bool)
+
+		for _, f := range r.File {
+			fName := filepath.ToSlash(f.Name)
+
+			// Check if this file is in the target directory
+			if dirPath == "" {
+				// List root level items
+				parts := strings.Split(fName, "/")
+				if len(parts) > 0 {
+					item := parts[0]
+					if item != "" && !seenItems[item] {
+						seenItems[item] = true
+						// Check if it's a directory
+						if len(parts) > 1 || f.FileInfo().IsDir() {
+							filelist = append(filelist, item+"/")
+						} else {
+							filelist = append(filelist, item)
+						}
+					}
+				}
+			} else if strings.HasPrefix(fName, dirPath) {
+				// Mark directory as existing
+				dirExists = true
+
+				// Get the relative path from the directory
+				relPath := strings.TrimPrefix(fName, dirPath)
+				if relPath != "" {
+					parts := strings.Split(relPath, "/")
+					if len(parts) > 0 {
+						item := parts[0]
+						if item != "" && !seenItems[item] {
+							seenItems[item] = true
+							// Check if it's a directory (has more parts or is a dir entry)
+							if len(parts) > 1 || (len(parts) == 1 && strings.HasSuffix(relPath, "/")) {
+								filelist = append(filelist, item+"/")
+							} else {
+								filelist = append(filelist, item)
+							}
+						}
+					}
+				}
+			}
+		}
+
+		// If directory path was specified but doesn't exist, return error
+		if dirPath != "" && !dirExists {
+			g.RaiseError(errors.New("Directory not found in zip: " + dirPath))
+			return otto.NullValue()
+		}
+
+		reply, _ := vm.ToValue(filelist)
+		return reply
+	})
+
+	// getFileFromZip(zipVpath, filePathInZip) => Extract specific file from zip to tmp:/
+	vm.Set("_ziplib_getFileFromZip", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		fileInZip, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		// Open zip file
+		r, err := zip.OpenReader(zipRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		defer r.Close()
+
+		// Find the file in zip
+		var targetFile *zip.File
+		for _, f := range r.File {
+			if filepath.ToSlash(f.Name) == filepath.ToSlash(fileInZip) {
+				targetFile = f
+				break
+			}
+		}
+
+		if targetFile == nil {
+			g.RaiseError(errors.New("File not found in zip: " + fileInZip))
+			return otto.NullValue()
+		}
+
+		// Extract to tmp
+		tmpFsh, err := u.GetFileSystemHandlerFromVirtualPath("tmp:/")
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		tmpFilename := arozfs.Base(fileInZip)
+		tmpVpath := "tmp:/" + tmpFilename
+		tmpRpath, _ := tmpFsh.FileSystemAbstraction.VirtualPathToRealPath(tmpVpath, u.Username)
+
+		rc, err := targetFile.Open()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		defer rc.Close()
+
+		err = tmpFsh.FileSystemAbstraction.WriteStream(tmpRpath, rc, 0755)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		u.SetOwnerOfFile(tmpFsh, tmpVpath)
+
+		reply, _ := vm.ToValue(tmpVpath)
+		return reply
+	})
+
+	// getCompressFileType(vpath) => Detect compression type
+	vm.Set("_ziplib_getCompressFileType", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
+
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+vpath))
+		}
+
+		_, rpath, err := static.VirtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		// Check file extension and magic bytes
+		ext := strings.ToLower(filepath.Ext(rpath))
+		fileType := "unknown"
+
+		switch ext {
+		case ".zip":
+			fileType = "zip"
+		case ".7z":
+			fileType = "7z"
+		case ".tar":
+			fileType = "tar"
+		case ".gz":
+			// Check if it's tar.gz
+			if strings.HasSuffix(strings.ToLower(rpath), ".tar.gz") || strings.HasSuffix(strings.ToLower(rpath), ".tgz") {
+				fileType = "tar.gz"
+			} else {
+				fileType = "gz"
+			}
+		case ".tgz":
+			fileType = "tar.gz"
+		default:
+			// Try to detect by magic bytes
+			f, err := os.Open(rpath)
+			if err == nil {
+				defer f.Close()
+				magic := make([]byte, 4)
+				f.Read(magic)
+
+				// Check magic bytes
+				if magic[0] == 0x50 && magic[1] == 0x4B && (magic[2] == 0x03 || magic[2] == 0x05) {
+					fileType = "zip"
+				} else if magic[0] == 0x37 && magic[1] == 0x7A && magic[2] == 0xBC && magic[3] == 0xAF {
+					fileType = "7z"
+				} else if magic[0] == 0x1F && magic[1] == 0x8B {
+					fileType = "gz"
+				}
+			}
+		}
+
+		reply, _ := vm.ToValue(fileType)
+		return reply
+	})
+
+	// extractAnyFile(sourceVpath, destVpath) => Extract based on file type detection
+	vm.Set("_ziplib_extractAnyFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		_, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Detect format
+		format, err := archiver.ByExtension(srcRpath)
+		if err != nil {
+			// Try by header
+			f, err := os.Open(srcRpath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			format, err = archiver.ByHeader(f)
+			f.Close()
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+		}
+
+		// Extract using appropriate archiver
+		if u, ok := format.(archiver.Unarchiver); ok {
+			err = u.Unarchive(srcRpath, destRpath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+		} else {
+			g.RaiseError(errors.New("format does not support extraction"))
+			return otto.FalseValue()
+		}
+
+		// Update ownership
+		destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
+			if err == nil && !info.IsDir() {
+				vp, _ := static.RealpathToVirtualpath(destFsh, path, u)
+				u.SetOwnerOfFile(destFsh, vp)
+			}
+			return nil
+		})
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// createAnyZipFile(sourceVpaths, outputVpath, format) => Create archive based on format
+	vm.Set("_ziplib_createAnyZipFile", func(call otto.FunctionCall) otto.Value {
+		sourcesObj, err := call.Argument(0).Export()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		outputVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		format, err := call.Argument(2).ToString()
+		if err != nil {
+			// Default to zip
+			format = "zip"
+		}
+
+		var sources []string
+		switch v := sourcesObj.(type) {
+		case []interface{}:
+			for _, s := range v {
+				if str, ok := s.(string); ok {
+					sources = append(sources, static.RelativeVpathRewrite(scriptFsh, str, vm, u))
+				}
+			}
+		case string:
+			sources = append(sources, static.RelativeVpathRewrite(scriptFsh, v, vm, u))
+		}
+
+		outputVpath = static.RelativeVpathRewrite(scriptFsh, outputVpath, vm, u)
+
+		for _, src := range sources {
+			if !u.CanRead(src) {
+				panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+src))
+			}
+		}
+		if !u.CanWrite(outputVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+outputVpath))
+		}
+
+		var sourceRpaths []string
+		for _, src := range sources {
+			_, rpath, err := static.VirtualPathToRealPath(src, u)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			sourceRpaths = append(sourceRpaths, rpath)
+		}
+
+		outputFsh, outputRpath, err := static.VirtualPathToRealPath(outputVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		// Create archive based on format
+		switch strings.ToLower(format) {
+		case "zip":
+			z := &archiver.Zip{}
+			err = z.Archive(sourceRpaths, outputRpath)
+		case "tar":
+			tar := &archiver.Tar{}
+			err = tar.Archive(sourceRpaths, outputRpath)
+		case "tar.gz", "tgz", "targz":
+			tgz := &archiver.TarGz{}
+			err = tgz.Archive(sourceRpaths, outputRpath)
+		case "gz", "gzip":
+			g.RaiseError(errors.New("gz format requires createGzFile for single file compression"))
+			return otto.FalseValue()
+		default:
+			g.RaiseError(errors.New("unsupported format: " + format))
+			return otto.FalseValue()
+		}
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		u.SetOwnerOfFile(outputFsh, outputVpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	vm.Run(`
+		var ziplib = {};
+		
+		ziplib.extractZipFile = _ziplib_extractZipFile;
+		ziplib.createZipFile = _ziplib_createZipFile;
+		ziplib.createTarFile = _ziplib_createTarFile;
+		ziplib.extractTarFile = _ziplib_extractTarFile;
+		ziplib.createTarGzFile = _ziplib_createTarGzFile;
+		ziplib.extractTarGzFile = _ziplib_extractTarGzFile;
+		ziplib.createGzFile = _ziplib_createGzFile;
+		ziplib.extractGzFile = _ziplib_extractGzFile;
+
+		// Generic functions
+		ziplib.isValidZipFile = _ziplib_isValidZipFile; // Check if file is valid archive file (zip, tar, tar.gz, gz)
+		ziplib.listZipFileContents = _ziplib_listZipFileContents; // List contents of zip in json tree structure
+		ziplib.listZipFileDir = _ziplib_listZipFileDir; // List contents of specific directory in zip
+		ziplib.getFileFromZip = _ziplib_getFileFromZip; // Get specific file from zip, stored temporary at tmp:/
+		ziplib.getCompressFileType = _ziplib_getCompressFileType;
+		ziplib.extractAnyFile = _ziplib_extractAnyFile; // Extract zip, tar, targz, gz based on file type
+		ziplib.createAnyZipFile = _ziplib_createAnyZipFile; // Create zip, tar, targz, gz based on input
+	`)
+}
+
+/*
+	Example Usages
+
+	// Extract a zip file to a destination folder
+	ziplib.extractZipFile("user:/documents/archive.zip", "user:/documents/extracted/");
+
+	// Create a zip file from multiple sources
+	ziplib.createZipFile(["user:/documents/file1.txt", "user:/documents/file2.txt"], "user:/backup.zip");
+	ziplib.createZipFile("user:/documents/single_file.txt", "user:/output.zip"); // Single file
+
+	// Create and extract tar archives
+	ziplib.createTarFile(["user:/data/folder1", "user:/data/folder2"], "user:/archive.tar");
+	ziplib.extractTarFile("user:/downloads/archive.tar", "user:/extracted/");
+
+	// Create and extract tar.gz archives
+	ziplib.createTarGzFile(["user:/logs/"], "user:/backup/logs.tar.gz");
+	ziplib.extractTarGzFile("user:/downloads/backup.tar.gz", "user:/restore/");
+
+	// Compress and decompress single files with gzip
+	ziplib.createGzFile("user:/documents/large_file.txt", "user:/compressed/large_file.txt.gz");
+	ziplib.extractGzFile("user:/compressed/data.gz", "user:/decompressed/data");
+
+	// Check if a file is a valid archive (supports zip, tar, tar.gz, gz)
+	if (ziplib.isValidZipFile("user:/downloads/file.zip")) {
+		console.log("Valid archive file");
+	}
+	if (ziplib.isValidZipFile("user:/downloads/backup.tar.gz")) {
+		console.log("Valid tar.gz archive");
+	}
+
+	// List zip contents as JSON tree structure
+	var contents = JSON.parse(ziplib.listZipFileContents("user:/archive.zip"));
+	console.log(contents); // Returns nested tree structure with files and folders
+
+	// List root contents of zip
+	var rootFiles = ziplib.listZipFileDir("user:/archive.zip", "");
+	console.log(rootFiles); // Returns ["folder1/", "folder2/", "file.txt"]
+
+	// List contents of specific folder in zip
+	var folderContents = ziplib.listZipFileDir("user:/archive.zip", "folder1");
+	console.log(folderContents); // Returns ["subfolder/", "file1.txt", "file2.txt"]
+
+	// List contents with path
+	var deepContents = ziplib.listZipFileDir("user:/archive.zip", "folder1/subfolder");
+	console.log(deepContents); // Returns only immediate children of folder1/subfolder/	// Extract a specific file from zip to tmp:/
+	var tmpPath = ziplib.getFileFromZip("user:/archive.zip", "folder1/important.txt");
+	console.log("File extracted to: " + tmpPath); // Returns "tmp:/important.txt"
+
+	// Detect compression type
+	var type = ziplib.getCompressFileType("user:/unknown_file.archive");
+	console.log("Archive type: " + type); // Returns "zip", "tar", "tar.gz", "gz", or "unknown"
+
+	// Extract any supported archive format (auto-detect)
+	ziplib.extractAnyFile("user:/downloads/archive.unknown", "user:/extracted/");
+
+	// Create archive with specific format
+	ziplib.createAnyZipFile(["user:/data/"], "user:/backup.zip", "zip");
+	ziplib.createAnyZipFile(["user:/data/"], "user:/backup.tar", "tar");
+	ziplib.createAnyZipFile(["user:/data/"], "user:/backup.tar.gz", "tar.gz");
+
+	// Error handling examples
+	try {
+		ziplib.extractZipFile("user:/nonexistent.zip", "user:/output/");
+	} catch (e) {
+		console.log("Error: " + e.message);
+	}
+
+	// Working with relative paths (when script is in user:/scripts/)
+	ziplib.createZipFile(["../documents/data.txt"], "../backup.zip"); // Relative to script location
+
+	// Batch operations
+	var filesToZip = [
+		"user:/documents/report.pdf",
+		"user:/documents/data.csv",
+		"user:/documents/images/"
+	];
+	ziplib.createZipFile(filesToZip, "user:/archive/batch_backup.zip");
+*/

+ 1 - 1
src/web/Photo/index.html

@@ -538,7 +538,7 @@
                 <button class="show-info-btn" onclick="showInfoPanel()">ℹ</button>
                 
                 <!-- Loading Progress -->
-                <div id="loading-progress" class="loading-progress">Loading 0%</div>
+                <div id="loading-progress" class="loading-progress"><i class="loading spinner icon"></i> Loading</div>
                 
                 <!-- Zoom Controls -->
                 <div id="zoom-controls" class="zoom-controls">

+ 36 - 111
src/web/Photo/photo.js

@@ -245,6 +245,8 @@ function closeViewer(){
     }, 300);
 }
 
+let compressedImageLoaded = false;
+let fullsizeImageLoaded = false;
 
 function showImage(object){
     // Reset zoom level when switching photos
@@ -256,21 +258,31 @@ function showImage(object){
         // Not an image card, do nothing
         return;
     }
+    
+    // Reset loading flags
+    compressedImageLoaded = false;
+    fullsizeImageLoaded = false;
+    
     var fd = JSON.parse(decodeURIComponent($(object).attr("filedata")));
     $("#info-dimensions").text("Calculating...");
     // Check if we should use compression (only for JPG/PNG > 5MB)
     const useCompression = shouldUseCompression(fd.filepath, fd.filesize);
 
+    // Set thumbnail as placeholder for full image
+    const thumbnailUrl = $(object).find('img').attr('src');
+    $("#fullImage").attr("src", thumbnailUrl);
+    $("#fullImage").hide();
+    $("#compressedImage").show();
+    $("#compressedImage").attr("src", thumbnailUrl);
+    $("#bg-image").attr("src", thumbnailUrl);
+    
     // Get image URL (backend handles RAW files automatically)
     getViewableImageUrl(fd.filepath, (imageUrl, isSupported, isBlob, method) => {
+        $("#loading-progress").show();
         const compressedImg = document.getElementById('compressedImage');
         const fullImg = document.getElementById('fullImage');
         const bgImg = document.getElementById('bg-image');
-        
-        // Reset compressed image
-        compressedImg.style.display = 'none';
-        compressedImg.classList.remove('hidden');
-         
+        $("#loading-progress").html(`<i class="loading spinner icon"></i> Loading`);
         if (useCompression) {
             // Use compressed version for large JPG/PNG files
             console.log('Large JPG/PNG detected (' + (fd.filesize / 1024 / 1024).toFixed(2) + 'MB), loading compressed version first');
@@ -286,13 +298,17 @@ function showImage(object){
                 })
             }).then(resp => {
                 resp.text().then(dataURL => {
-                    // Show compressed image
-                    compressedImg.src = dataURL;
-                    compressedImg.style.display = 'block';
-                    bgImg.src = dataURL;
-
-                    // Start loading full-size image in background
-                    loadFullSizeImageInBackground(imageUrl, fd);
+                    $("#loading-progress").html(`<i class="loading spinner icon"></i> Optimizing Resolution`);
+                    compressedImageLoaded = true;
+
+                    // Only show compressed image if full-size hasn't loaded yet
+                    if (!fullsizeImageLoaded) {
+                        compressedImg.src = dataURL;
+                        compressedImg.style.display = 'block';
+                        bgImg.src = dataURL;
+                    } else {
+                        console.log('Full-size image already loaded, skipping compressed image display');
+                    }
                 });
             }).catch(error => {
                 console.error('Failed to load compressed image:', error);
@@ -300,8 +316,13 @@ function showImage(object){
                 fullImg.src = imageUrl;
                 bgImg.src = imageUrl;
             });
+
+            // Start loading full-size image in background
+            loadFullSizeImageInBackground(imageUrl, fd);
         } else {
             $("#compressedImage").hide();
+            $("#fullImage").show();
+            $("#loading-progress").hide();
             // Use full image URL directly for RAW, WEBP, or small JPG/PNG files
             if (method === 'backend_raw') {
                 console.log('RAW file: Rendered by backend');
@@ -312,14 +333,15 @@ function showImage(object){
 
         // Update image dimensions and generate histogram when full image loads
         $("#fullImage").off("load").on('load', function() {
+            fullsizeImageLoaded = true;
             let width = this.naturalWidth;
             let height = this.naturalHeight;
             $("#info-dimensions").text(width + ' × ' + height + "px");
 
             // Hide the compressed image once full image is loaded
             $("#compressedImage").hide();
-
-            // Wait for image to be ready, then generate histogram
+            $("#fullImage").show();
+            $("#loading-progress").hide();
             const canvas = document.getElementById('histogram-canvas');
             if (canvas) {
                 generateHistogram(this, canvas);
@@ -377,105 +399,8 @@ function showImage(object){
 // Function to load full-size image in background with progress tracking
 function loadFullSizeImageInBackground(fullSizeUrl, fileData) {
     console.log('Starting background download of full-size image...');
-    
-  
-
-    const loadingIndicator = document.getElementById('loading-progress');
     const fullImage = document.getElementById('fullImage');
-    const compressedImage = document.getElementById('compressedImage');
-    const bgImage = document.getElementById('bg-image');
     fullImage.src = fullSizeUrl;
-
-    return;
-    // Legacy blob loading method
-    // Show loading indicator
-    /*
-    if (loadingIndicator) {
-        loadingIndicator.style.display = 'block';
-        loadingIndicator.textContent = 'Loading 0%';
-    }
-    
-    const xhr = new XMLHttpRequest();
-    xhr.open('GET', fullSizeUrl, true);
-    xhr.responseType = 'arraybuffer';
-    
-    // Track download progress
-    xhr.onprogress = function(event) {
-        if (event.lengthComputable) {
-            const percentComplete = (event.loaded / event.total) * 100;
-            console.log('Download progress: ' + percentComplete.toFixed(2) + '% (' + 
-                       (event.loaded / 1024 / 1024).toFixed(2) + 'MB / ' + 
-                       (event.total / 1024 / 1024).toFixed(2) + 'MB)');
-            
-            // Update loading indicator
-            if (loadingIndicator) {
-                loadingIndicator.textContent = 'Loading ' + Math.round(percentComplete) + '%';
-            }
-        } else {
-            console.log('Download progress: ' + (event.loaded / 1024 / 1024).toFixed(2) + 'MB downloaded');
-            
-            // Update loading indicator without percentage
-            if (loadingIndicator) {
-                loadingIndicator.textContent = 'Loading...';
-            }
-        }
-    };
-    
-    // Handle successful download
-    xhr.onload = function() {
-        // Hide loading indicator
-        if (loadingIndicator) {
-            loadingIndicator.style.display = 'none';
-        }
-        
-        if (xhr.status === 200) {
-            console.log('Full-size image downloaded successfully, swapping images...');
-            
-            // Set full image src directly (browser will cache it)
-            fullImage.onload = function() {
-                console.log('Full-size image loaded and cached');
-                
-                // Fade out compressed image
-                if (compressedImage.style.display !== 'none') {
-                    compressedImage.classList.add('hidden');
-                    setTimeout(() => {
-                        compressedImage.style.display = 'none';
-                    }, 300); // Match CSS transition duration
-                }
-                
-                // Update background
-                bgImage.src = fullSizeUrl;
-                
-                // Update dimensions with full-size image dimensions
-                $("#info-dimensions").text(fullImage.naturalWidth + ' × ' + fullImage.naturalHeight + "px");
-                
-                // Regenerate histogram with full-size image
-                const canvas = document.getElementById('histogram-canvas');
-                if (canvas) {
-                    generateHistogram(fullImage, canvas);
-                }
-            };
-            
-            // Set the source to trigger browser caching
-            fullImage.src = fullSizeUrl;
-        }
-    };
-    
-    // Handle errors
-    xhr.onerror = function() {
-        // Hide loading indicator
-        if (loadingIndicator) {
-            loadingIndicator.style.display = 'none';
-        }
-        console.error('Failed to download full-size image');
-        
-        // Try to load directly as fallback
-        fullImage.src = fullSizeUrl;
-    };
-    
-    // Start the download
-    xhr.send();
-    */
 }
 
 $(document).on("keydown", function(e){

+ 12 - 0
src/web/desktop.html

@@ -5264,6 +5264,7 @@
                           
                             }else{
                                 addContextMenuItem($("#contextmenu"), lcontex('Open'), undefined, "openIconViaMenu", false);
+                                addContextMenuItem($("#contextmenu"), lcontex('Open in New Tab'), "<i class='external icon'></i>", "openURLShortcutInNewTab", false);
                                 addContextMenuItem($("#contextmenu"), lcontex('Delete'), "<i class='trash icon'></i>", "handleFileDelete", false);
                             }
                             
@@ -5398,6 +5399,17 @@
             hideAllContextMenus();
         }
 
+        function openURLShortcutInNewTab(target, event){
+            $(".launchIconWrapper.selected").each(function(){
+                var fd = JSON.parse(decodeURIComponent($(this).parent().attr("filedata")));
+                if (fd.ShortcutType == "url" && fd.ShortcutPath) {
+                    window.open(fd.ShortcutPath);
+                }
+            });
+
+            hideAllContextMenus();
+        }
+
         function downloadFile(target, event){
             if ( $(".launchIconWrapper.selected").length == 1){
                 //Download this file directly