瀏覽代碼

Added advance folder thumbnail feature

Toby Chui 6 小時之前
父節點
當前提交
8c25186e56

+ 189 - 106
mod/filesystem/metadata/folder.go

@@ -1,106 +1,189 @@
-package metadata
-
-import (
-	"errors"
-	"image"
-	"image/draw"
-	"image/jpeg"
-	"image/png"
-	"log"
-	"path/filepath"
-
-	"github.com/nfnt/resize"
-	"imuslab.com/arozos/mod/filesystem"
-)
-
-/*
-	Generate folder thumbnail from the containing files
-	The preview is generated by overlapping 2 - 3 layers of images
-*/
-
-func generateThumbnailForFolder(fsh *filesystem.FileSystemHandler, cacheFolder string, file string, generateOnly bool) (string, error) {
-	if fsh.RequireBuffer {
-		return "", nil
-	}
-	fshAbs := fsh.FileSystemAbstraction
-	//Check if this folder has cache image folder
-	cacheFolderInsideThisFolder := filepath.Join(file, "/.metadata/.cache")
-	if !fshAbs.FileExists(cacheFolderInsideThisFolder) {
-		//This folder do not have a cache folder
-		return "", errors.New("No previewable files")
-	}
-
-	//Load the base template
-	if !fshAbs.FileExists("web/img/system/folder-preview.png") {
-		//Missing system files. Skip rendering
-		return "", errors.New("Missing system template image file")
-	}
-	image1, err := fshAbs.Open("web/img/system/folder-preview.png")
-	if err != nil {
-		return "", err
-	}
-
-	baseTemplate, err := png.Decode(image1)
-	if err != nil {
-		return "", err
-	}
-	image1.Close()
-
-	//Generate the base image
-	b := baseTemplate.Bounds()
-	resultThumbnail := image.NewRGBA(b)
-	draw.Draw(resultThumbnail, b, baseTemplate, image.ZP, draw.Over)
-
-	//Get cached file inside this folder, only include jpg (non folder)
-	contentCache, _ := fshAbs.Glob(filepath.Join(cacheFolderInsideThisFolder, "/*.jpg"))
-
-	//Check if there are more than 1 file inside this folder that is cached
-	if len(contentCache) > 1 {
-		//More than 1 files. Render the image at the back
-		image2, err := fshAbs.Open(contentCache[1])
-		if err != nil {
-			return "", err
-		}
-		backImage, err := jpeg.Decode(image2)
-		if err != nil {
-			return "", err
-		}
-		backImgOffset := image.Pt(155, 110)
-		defer image2.Close()
-		resizedBackImg := resize.Resize(250, 250, backImage, resize.Lanczos3)
-		draw.Draw(resultThumbnail, resizedBackImg.Bounds().Add(backImgOffset), resizedBackImg, image.ZP, draw.Over)
-	} else {
-		//Nothing to preview inside this folder
-		return "", errors.New("No previewable files")
-	}
-
-	//Render the top image
-	image3, err := fshAbs.Open(contentCache[0])
-	if err != nil {
-		return "", errors.New("failed to open: " + err.Error())
-	}
-
-	topImage, err := jpeg.Decode(image3)
-	if err != nil {
-		//Fail to decode the image. Try to remove the damaged iamge file
-		image3.Close()
-		fshAbs.Remove(contentCache[0])
-		log.Println("Failed to decode cahce image for: " + contentCache[0] + ". Removing thumbnail cache")
-		return "", errors.New("failed to decode: " + err.Error())
-	}
-	defer image3.Close()
-
-	topImageOffset := image.Pt(210, 210)
-	resizedTopImage := resize.Resize(260, 260, topImage, resize.Lanczos3)
-	draw.Draw(resultThumbnail, resizedTopImage.Bounds().Add(topImageOffset), resizedTopImage, image.ZP, draw.Over)
-
-	outfile, err := fshAbs.Create(filepath.Join(cacheFolder, filepath.Base(file)+".png"))
-	if err != nil {
-		log.Fatalf("failed to create: %s", err)
-	}
-	png.Encode(outfile, resultThumbnail)
-	outfile.Close()
-
-	ctx, err := getImageAsBase64(fsh, cacheFolder+filepath.Base(file)+".png")
-	return ctx, err
-}
+package metadata
+
+import (
+	"encoding/base64"
+	"errors"
+	"image"
+	"image/draw"
+	"image/jpeg"
+	"image/png"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/nfnt/resize"
+	"imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/utils"
+)
+
+/*
+	Generate folder thumbnail from the containing files
+	The preview is generated by overlapping 2 - 3 layers of images
+*/
+
+func generateThumbnailForFolder(fsh *filesystem.FileSystemHandler, cacheFolder string, file string, generateOnly bool) (string, error) {
+	if fsh.RequireBuffer {
+		return "", nil
+	}
+	fshAbs := fsh.FileSystemAbstraction
+	//Check if this folder has cache image folder
+	cacheFolderInsideThisFolder := filepath.Join(file, "/.metadata/.cache")
+	if !fshAbs.FileExists(cacheFolderInsideThisFolder) {
+		//This folder do not have a cache folder
+		succ := generateLayeredThumbnailFolder(fsh, file)
+		if !succ {
+			return "", errors.New("failed to generate layered thumbnails inside folder")
+		}
+	}
+
+	//Load the base template
+	if !utils.FileExists("web/img/system/folder-preview.png") {
+		//Missing system files. Skip rendering
+		return "", errors.New("missing system template image file")
+	}
+	image1, err := os.Open("web/img/system/folder-preview.png")
+	if err != nil {
+		return "", err
+	}
+
+	baseTemplate, err := png.Decode(image1)
+	if err != nil {
+		return "", err
+	}
+	image1.Close()
+
+	//Generate the base image
+	b := baseTemplate.Bounds()
+	resultThumbnail := image.NewRGBA(b)
+	draw.Draw(resultThumbnail, b, baseTemplate, image.ZP, draw.Over)
+
+	//Get cached file inside this folder, only include jpg (non folder)
+	contentCache, _ := fshAbs.Glob(filepath.Join(cacheFolderInsideThisFolder, "/*.jpg"))
+
+	//Check if there are more than 1 file inside this folder that is cached
+	if len(contentCache) > 1 {
+		//More than 1 files. Render the image at the back
+		image2, err := fshAbs.Open(contentCache[1])
+		if err != nil {
+			return "", err
+		}
+		backImage, err := jpeg.Decode(image2)
+		if err != nil {
+			return "", err
+		}
+		backImgOffset := image.Pt(155, 110)
+		defer image2.Close()
+		resizedBackImg := resize.Resize(250, 250, backImage, resize.Lanczos3)
+		draw.Draw(resultThumbnail, resizedBackImg.Bounds().Add(backImgOffset), resizedBackImg, image.Point{}, draw.Over)
+	} else {
+		//Nothing to preview inside this folder, check if the folder is really empty
+		filesInside, _ := fshAbs.ReadDir(file)
+		if len(filesInside) > 1 {
+			//Not only the metadata folder is inside
+			templateFile := "web/img/system/folder-content.png"
+			if utils.FileExists(templateFile) {
+				content, err := os.ReadFile(templateFile)
+				if err != nil {
+					return "", err
+				}
+				encoded := base64.StdEncoding.EncodeToString(content)
+				return string(encoded), nil
+			} else {
+				return "", errors.New("missing system template image file")
+			}
+		}
+
+		return "", errors.New("no previewable files")
+	}
+
+	//Render the top image
+	image3, err := fshAbs.Open(contentCache[0])
+	if err != nil {
+		return "", errors.New("failed to open: " + err.Error())
+	}
+
+	topImage, err := jpeg.Decode(image3)
+	if err != nil {
+		//Fail to decode the image. Try to remove the damaged iamge file
+		image3.Close()
+		fshAbs.Remove(contentCache[0])
+		log.Println("Failed to decode cahce image for: " + contentCache[0] + ". Removing thumbnail cache")
+		return "", errors.New("failed to decode: " + err.Error())
+	}
+	defer image3.Close()
+
+	topImageOffset := image.Pt(210, 210)
+	resizedTopImage := resize.Resize(260, 260, topImage, resize.Lanczos3)
+	draw.Draw(resultThumbnail, resizedTopImage.Bounds().Add(topImageOffset), resizedTopImage, image.Point{}, draw.Over)
+
+	outfile, err := fshAbs.Create(filepath.Join(cacheFolder, filepath.Base(file)+".png"))
+	if err != nil {
+		log.Fatalf("failed to create: %s", err)
+	}
+	png.Encode(outfile, resultThumbnail)
+	outfile.Close()
+
+	ctx, err := getImageAsBase64(fsh, cacheFolder+filepath.Base(file)+".png")
+	return ctx, err
+}
+
+// generateLayeredThumbnailFolder generate 2 thumbnails inside that folder
+// for folder thumbnail render
+func generateLayeredThumbnailFolder(fsh *filesystem.FileSystemHandler, file string) bool {
+	if fsh.RequireBuffer {
+		//Too much work for buffer mode, skip generating
+		return false
+	}
+	fshAbs := fsh.FileSystemAbstraction
+	//Check if this folder has cache image folder
+	cacheFolderInsideThisFolder := filepath.Join(file, "/.metadata/.cache")
+	if !fshAbs.FileExists(cacheFolderInsideThisFolder) {
+		//Create the cache folder
+		err := fshAbs.MkdirAll(cacheFolderInsideThisFolder, 0755)
+		if err != nil {
+			return false
+		}
+	}
+
+	//List all files in the folder
+	files, err := fshAbs.ReadDir(file)
+	if err != nil {
+		return false
+	}
+
+	//Generate thumbnails for the first 2 image files
+	imageExtensions := []string{".jpg", ".jpeg", ".png"}
+	generatedCount := 0
+	for _, fi := range files {
+		if fi.IsDir() || fi.Name()[0] == '.' {
+			continue
+		}
+		ext := strings.ToLower(filepath.Ext(fi.Name()))
+		isImage := false
+		for _, imgExt := range imageExtensions {
+			if ext == imgExt {
+				isImage = true
+				break
+			}
+		}
+		if isImage {
+			filePath := filepath.Join(file, fi.Name())
+			// Generate thumbnail for this image into the cache folder
+			_, err := generateThumbnailForImage(fsh, cacheFolderInsideThisFolder+"/", filePath, false)
+			if err == nil {
+				generatedCount++
+				if generatedCount >= 2 {
+					break
+				}
+			}
+		}
+	}
+
+	if generatedCount == 0 {
+		// -1 because there always will be at least 1 folder (the metadata folder)
+		return false
+	}
+
+	return true
+}

+ 30 - 23
mod/filesystem/metadata/metadata.go

@@ -83,55 +83,62 @@ func (rh *RenderHandler) LoadCache(fsh *filesystem.FileSystemHandler, rpath stri
 	hidden.HideFile(filepath.Dir(filepath.Clean(cacheFolder)))
 	hidden.HideFile(cacheFolder)
 
-	//Check if cache already exists. If yes, return the image from the cache folder
+	needsGen, cacheData, err := rh.checkCacheNeeded(fsh, rpath, generateOnly, cacheFolder)
+	if err != nil {
+		return "", err
+	}
+	if !needsGen {
+		return cacheData, nil
+	}
+
+	//Generate
+	return rh.generateCache(fsh, cacheFolder, rpath, generateOnly)
+}
+
+// checkCacheNeeded checks if cache generation is needed
+func (rh *RenderHandler) checkCacheNeeded(fsh *filesystem.FileSystemHandler, rpath string, generateOnly bool, cacheFolder string) (needsGeneration bool, cacheData string, err error) {
 	if CacheExists(fsh, rpath) {
 		if generateOnly {
-			//Only generate, do not return image
-			return "", nil
+			return false, "", nil
 		}
 
-		//Allow thumbnail to be either jpg or png file
 		ext := ".jpg"
-		if !fshAbs.FileExists(cacheFolder + filepath.Base(rpath) + ".jpg") {
+		if !fsh.FileSystemAbstraction.FileExists(cacheFolder + filepath.Base(rpath) + ".jpg") {
 			ext = ".png"
 		}
 
-		//Updates 02/10/2021: Check if the source file is newer than the cache. Update the cache if true
-		folderModeTime, _ := fshAbs.GetModTime(rpath)
-		cacheImageModeTime, _ := fshAbs.GetModTime(cacheFolder + filepath.Base(rpath) + ext)
+		folderModeTime, _ := fsh.FileSystemAbstraction.GetModTime(rpath)
+		cacheImageModeTime, _ := fsh.FileSystemAbstraction.GetModTime(cacheFolder + filepath.Base(rpath) + ext)
 		if folderModeTime > cacheImageModeTime {
-			//File is newer than cache. Delete the cache
-			fshAbs.Remove(cacheFolder + filepath.Base(rpath) + ext)
+			fsh.FileSystemAbstraction.Remove(cacheFolder + filepath.Base(rpath) + ext)
+			return true, "", nil
 		} else {
-			//Check if the file is being writting by another process. If yes, wait for it
 			counter := 0
 			for rh.fileIsBusy(rpath) && counter < 15 {
 				counter += 1
 				time.Sleep(1 * time.Second)
 			}
 
-			//Time out and the file is still busy
 			if rh.fileIsBusy(rpath) {
-				log.Println("Process racing for cache file. Skipping", filepath.Base(rpath))
-				return "", errors.New("Process racing for cache file. Skipping")
+				return false, "", errors.New("process racing for cache file")
 			}
 
-			//Read and return the image
 			ctx, err := getImageAsBase64(fsh, cacheFolder+filepath.Base(rpath)+ext)
-			return ctx, err
+			return false, ctx, err
 		}
 
 	} else if fsh.ReadOnly {
-		//Not exists, but this Fsh is read only. Return nothing
-		return "", errors.New("Cannot generate thumbnail on readonly file system")
+		return false, "", errors.New("cannot generate thumbnail on readonly file system")
 	} else {
-		//This file not exists yet. Check if it is being hold by another process already
 		if rh.fileIsBusy(rpath) {
-			log.Println("Process racing for cache file. Skipping", filepath.Base(rpath))
-			return "", errors.New("Process racing for cache file. Skipping")
+			return false, "", errors.New("process racing for cache file")
 		}
 	}
 
+	return true, "", nil
+}
+
+func (rh *RenderHandler) generateCache(fsh *filesystem.FileSystemHandler, cacheFolder string, rpath string, generateOnly bool) (string, error) {
 	//Cache image not exists. Set this file to busy
 	rh.renderingFiles.Store(rpath, "busy")
 
@@ -183,7 +190,7 @@ func (rh *RenderHandler) LoadCache(fsh *filesystem.FileSystemHandler, rpath stri
 	}
 
 	//Folder preview renderer
-	if fshAbs.IsDir(rpath) && len(filepath.Base(rpath)) > 0 && filepath.Base(rpath)[:1] != "." {
+	if fsh.FileSystemAbstraction.IsDir(rpath) && len(filepath.Base(rpath)) > 0 && filepath.Base(rpath)[:1] != "." {
 		img, err := generateThumbnailForFolder(fsh, cacheFolder, rpath, generateOnly)
 		rh.renderingFiles.Delete(rpath)
 		return img, err
@@ -191,7 +198,7 @@ func (rh *RenderHandler) LoadCache(fsh *filesystem.FileSystemHandler, rpath stri
 
 	//Other filters
 	rh.renderingFiles.Delete(rpath)
-	return "", errors.New("No supported format")
+	return "", errors.New("no supported format")
 }
 
 func (rh *RenderHandler) fileIsBusy(path string) bool {

+ 16 - 11
web/OfficeViewer/embedded.html

@@ -67,11 +67,10 @@
   </style>
 </head>
 <body>
-  <div id="fileOpenDetails" class="ui container" style="margin-top: 0.4em;">
-    <p>File Rendered:  <a href="" id="filename" target="_blank"></a></p>
-  </div>
   <div id="resolte-contaniner">
-    
+    <div id="loading" style="display:flex;align-items:center;justify-content:center;min-height:150px;font-size:1.1rem;color:#666">
+      loading
+    </div>
   </div>
   <script>
     //Get the input file from the hash
@@ -98,13 +97,19 @@
       window.location.href = "./index.html"
     }
     function loadFile(file_path){
-        $("#resolte-contaniner").officeToHtml({
-          url: file_path,
-          pdfSetting: {
-            setLang: "",
-            setLangFilesPath: ""
-          }
-        });
+        try {
+          var error = $("#resolte-contaniner").officeToHtml({
+            url: file_path,
+            pdfSetting: {
+              setLang: "",
+              setLangFilesPath: ""
+            }
+          });
+        } catch (err) {
+          console.error(err);
+          $("#loading").text("Error loading file: " + (err && err.message ? err.message : String(err)));
+          $("#loading").css("color", "red");
+        }
       }
   </script>
 </body>

文件差異過大導致無法顯示
+ 27 - 0
web/img/system/folder-content.ai


二進制
web/img/system/folder-content.png


+ 17 - 0
web/img/system/folder-content.svg

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="圖層_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
+<rect x="8.795" y="15.321" fill="#A6834E" width="63.372" height="99.27"/>
+<g id="圖層_3">
+	<polygon fill="#DCDDDD" points="51.698,28.194 29.746,50.146 29.746,103.5 92.255,111.682 90.82,33.983 	"/>
+	<polygon fill="#EFEFEF" points="38.116,33.284 15.169,56.231 15.169,112.002 80.511,120.553 79.011,39.334 	"/>
+</g>
+<g>
+	<polygon fill="#E2B04F" points="62.34,80.304 62.34,26.121 25.898,21.083 25.898,116.082 51.018,119.553 51.018,84.175 	"/>
+</g>
+<g>
+	<polygon fill="#EEC363" points="38.912,77.203 38.912,20.585 8.795,15.321 8.795,114.591 29.556,118.219 29.556,81.25 	"/>
+</g>
+</svg>

部分文件因文件數量過多而無法顯示