diff --git a/Tiefsee/Lib/FileLib.cs b/Tiefsee/Lib/FileLib.cs index 03d20cae..d782f626 100644 --- a/Tiefsee/Lib/FileLib.cs +++ b/Tiefsee/Lib/FileLib.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.IO; using System.Runtime.InteropServices; using System.Text; @@ -195,12 +194,13 @@ public static string FileToHash(string path) { using var sha256 = System.Security.Cryptography.SHA256.Create(); string s; if (File.Exists(path)) { - long fileSize = new FileInfo(path).Length; // File size - long ticks = new FileInfo(path).LastWriteTime.Ticks; // File last modified time + var fileinfo = new FileInfo(path); + long fileSize = fileinfo.Length; // File size + long ticks = fileinfo.LastWriteTime.Ticks; // File last modified time s = Convert.ToBase64String(sha256.ComputeHash(Encoding.Default.GetBytes(fileSize + path + ticks))); } else { - s = path; + s = Convert.ToBase64String(sha256.ComputeHash(Encoding.Default.GetBytes(path))); } return s.ToLower().Replace("\\", "").Replace("/", "").Replace("+", "").Replace("=", ""); } diff --git a/Tiefsee/Lib/ImgLib.cs b/Tiefsee/Lib/ImgLib.cs index 4aee2da9..99f38207 100644 --- a/Tiefsee/Lib/ImgLib.cs +++ b/Tiefsee/Lib/ImgLib.cs @@ -19,17 +19,21 @@ public class ImgLib { /// 取得任何檔案的圖示 /// /// - /// 16 32 64 128 256 + /// 16 32 64 128 256 + /// 等待秒數 /// - public static Bitmap GetFileIcon(string path, int size) { + public static Bitmap GetFileIcon(string path, int size, double waitSec = 1.5) { if (File.Exists(path) == false) { return null; } Bitmap icon = null; - Adapter.RunWithTimeout(1, () => { - // 取得圖片在Windows系統的縮圖 - icon = WindowsThumbnailProvider.GetThumbnail(path, size, size, ThumbnailOptions.ScaleUp); - }); + try { + Adapter.RunWithTimeout(waitSec, () => { + // 取得圖片在Windows系統的縮圖 + icon = WindowsThumbnailProvider.GetThumbnail(path, size, size, ThumbnailOptions.ScaleUp); + }); + } + catch { } return icon; } diff --git a/Tiefsee/Server/WebServerController.cs b/Tiefsee/Server/WebServerController.cs index 805f8365..c8f933ef 100644 --- a/Tiefsee/Server/WebServerController.cs +++ b/Tiefsee/Server/WebServerController.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Net.Http; using System.Text; using System.Text.Json; @@ -27,6 +29,7 @@ public WebServerController(WebServer ws) { webServer.RouteAdd("/api/getPdf", GetPdf); webServer.RouteAdd("/api/getText", GetText); webServer.RouteAdd("/api/getFileIcon", GetFileIcon); + webServer.RouteAdd("/api/getWebIcon", GetWebIcon); webServer.RouteAdd("/api/getFileInfo2", GetFileInfo2); webServer.RouteAdd("/api/getFileInfo2List", GetFileInfo2List); webServer.RouteAdd("/api/getUwpList", GetUwpList); @@ -337,14 +340,14 @@ private void GetPdf(RequestData d) { } /// - /// 取得檔案的Exif資訊 + /// 取得檔案的 Exif 資訊 /// void GetText(RequestData d) { string path = d.args["path"]; path = Uri.UnescapeDataString(path); - //如果檔案不存在就返回404錯誤 + //如果檔案不存在就返回 404 錯誤 if (File.Exists(path) == false) { d.context.Response.StatusCode = 404; WriteString(d, JsonSerializer.Serialize(new ImgExif())); @@ -360,7 +363,7 @@ void GetText(RequestData d) { } /// - /// + /// 取得檔案的 Icon /// private void GetFileIcon(RequestData d) { @@ -370,8 +373,7 @@ private void GetFileIcon(RequestData d) { // 如果檔案不存在就返回 404 錯誤 if (File.Exists(path) == false) { - d.context.Response.StatusCode = 404; - WriteString(d, "404"); + WriteError(d, 404, "找不到檔案"); return; } @@ -379,50 +381,90 @@ private void GetFileIcon(RequestData d) { bool is304 = HeadersAdd304(d, path); // 回傳檔案時加入快取的 Headers if (is304) { return; } - Bitmap icon = null; + using Bitmap icon = ImgLib.GetFileIcon(path, size, 3); + + // 如果取得失敗,就返回 500 錯誤 + if (icon == null) { + WriteError(d, 500, "圖示取得失敗"); + return; + } try { - icon = ImgLib.GetFileIcon(path, size); + using Stream input = new MemoryStream(); + icon.Save(input, System.Drawing.Imaging.ImageFormat.Png); + input.Position = 0; + + WriteStream(d, input); // 回傳檔案 + + icon.Dispose(); + } + catch { + WriteError(d, 500, "圖示解析失敗"); } - catch { } + } - // 如果取得失敗,就等待 1 秒後再試一次 - if (icon == null) { - Thread.Sleep(1000); + /// + /// 從網路下載圖片後,返回圖片的 icon + /// + /// + private async void GetWebIcon(RequestData d) { + + var args = d.args; + int size = Int32.Parse(args["size"]); + string path = Uri.UnescapeDataString(args["path"]); // 檔案儲存的相對路徑 + string url = Uri.UnescapeDataString(args["url"]); + + string tempPath = Path.Combine(AppPath.tempDirWebFile, path); + + // 如果資料夾不存在就建立 + if (Directory.Exists(Path.GetDirectoryName(tempPath)) == false) { + Directory.CreateDirectory(Path.GetDirectoryName(tempPath)); + } + + if (File.Exists(tempPath) == false) { try { - icon = ImgLib.GetFileIcon(path, size); + // 下載圖片 + using HttpClient webClient = new(); + webClient.Timeout = TimeSpan.FromSeconds(10); // 設定超時時間為 10 秒 + byte[] data = webClient.GetByteArrayAsync(url).Result; + File.WriteAllBytes(tempPath, data); + } + catch (Exception ex) { + Debug.WriteLine("GetWebIcon fail " + ex.Message); + WriteError(d, 500, "圖片下載失敗: " + ex); + return; } - catch { } } - // 如果 2 次都取得失敗,就返回 500 錯誤 + // 如果檔案不存在就返回 404 錯誤 + if (File.Exists(tempPath) == false) { + WriteError(d, 404, "找不到檔案"); + return; + } + + d.context.Response.ContentType = "image/png"; + bool is304 = HeadersAdd304(d, tempPath); // 回傳檔案時加入快取的 Headers + if (is304) { return; } + + using Bitmap icon = ImgLib.GetFileIcon(tempPath, size, 3); + + // 如果取得失敗,就返回 500 錯誤 if (icon == null) { - d.context.Response.StatusCode = 500; - WriteString(d, "500"); + WriteError(d, 500, "圖示取得失敗"); return; } try { using Stream input = new MemoryStream(); - icon.Save(input, System.Drawing.Imaging.ImageFormat.Png); input.Position = 0; - d.context.Response.ContentLength64 = input.Length; + WriteStream(d, input); // 回傳檔案 - if (d.context.Request.HttpMethod != "HEAD") { - byte[] buffer = new byte[1024 * 16]; - int nbytes; - while ((nbytes = input.Read(buffer, 0, buffer.Length)) > 0) { - // context.Response.SendChunked = input.Length > 1024 * 16; - d.context.Response.OutputStream.Write(buffer, 0, nbytes); - } - } icon.Dispose(); } catch { - d.context.Response.StatusCode = 500; - WriteString(d, "500"); + WriteError(d, 500, "圖示解析失敗"); } } @@ -700,11 +742,17 @@ private bool HeadersAdd304(RequestData d, string path) { return false; } + /// + /// 回傳錯誤 + /// + private void WriteError(RequestData d, int code, string msg) { + d.context.Response.StatusCode = code; + WriteString(d, msg); + } + /// /// 回傳字串 /// - /// - /// private void WriteString(RequestData d, string str) { d.context.Response.AddHeader("Content-Encoding", "br"); // 告訴瀏覽器使用了Brotli壓縮 d.context.Response.AddHeader("Content-Type", "text/text; charset=utf-8"); //設定編碼 @@ -738,15 +786,7 @@ private void WriteFile(RequestData d, string path) { d.context.Response.OutputStream.Write(buffer, 0, nbytes); } } - // context.Response.StatusCode = (int)HttpStatusCode.OK; - // context.Response.OutputStream.Flush(); } - /*using (FileStream fs = new FileStream(_path, FileMode.Open, FileAccess.Read, FileAccess.Read, FileShare.ReadWrite)) { - byte[] _responseArray = new byte[fs.Length]; - fs.Read(_responseArray, 0, _responseArray.Length); - fs.Close(); - context.Response.OutputStream.Write(_responseArray, 0, _responseArray.Length); // write bytes to the output stream - }*/ } /// @@ -764,7 +804,6 @@ void WriteStream(RequestData d, Stream ms) { } } - private IDictionary _mimeTypeMappings = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { #region extension to MIME type list {".asf", "video/x-ms-asf"}, diff --git a/Tiefsee/WebWindow.cs b/Tiefsee/WebWindow.cs index 35402ad4..a44f84fb 100644 --- a/Tiefsee/WebWindow.cs +++ b/Tiefsee/WebWindow.cs @@ -237,6 +237,7 @@ public static string GetAppInfo(string[] args, int quickLookRunType) { startPort = Program.startPort, appDirPath = System.AppDomain.CurrentDomain.BaseDirectory, appDataPath = AppPath.appData, + tempDirWebFile = AppPath.tempDirWebFile, mainPort = Program.webServer.port, settingPath = AppPath.appDataSetting, quickLookRunType = quickLookRunType, @@ -266,6 +267,8 @@ public class AppInfo { public string appDirPath { get; set; } /// 程式的暫存資料夾 public string appDataPath { get; set; } + /// 暫存資料夾 - 從網路下載的檔案 + public string tempDirWebFile { get; set; } /// 目前使用的 port public int mainPort { get; set; } /// setting.js 的路徑 diff --git a/Www/ejs/SettingWindow/SettingWindow.ejs b/Www/ejs/SettingWindow/SettingWindow.ejs index e0ed4a0a..bd87d7f8 100644 --- a/Www/ejs/SettingWindow/SettingWindow.ejs +++ b/Www/ejs/SettingWindow/SettingWindow.ejs @@ -294,8 +294,8 @@ -
- 檔案刪除前顯示確認視窗 +
+ 檔案刪除前顯示確認視窗
-
- 偵測到檔案新增時,插入於 +
+ 偵測到檔案新增時,插入於
+ +
顯示 Civitai Resources
+
+ +
+
+
圖片預設狀態
+
+ +
+ +
圖片數量
+
+ +
+ +
允許 NSFW 圖片
+
+ +
+ +
+
diff --git a/Www/lang/langData.js b/Www/lang/langData.js index 2033e1a2..7c0debb8 100644 --- a/Www/lang/langData.js +++ b/Www/lang/langData.js @@ -624,8 +624,28 @@ var langData = { "zh-TW": `自動找出相同檔名的檔案。
例如 "dog.jpg", "dog.txt", "dog.preview.png"`, "en": `Automatically find files with the same file name.
For example, "dog.jpg", "dog.txt", "dog.preview.png"`, "ja": `同じファイル名のファイルを自動的に見つけます。
例えば、"dog.jpg", "dog.txt", "dog.preview.png"` - } + }, + civitaiResourcesEnabled: { + "zh-TW": `顯示 Civitai Resources`, + "en": `Display Civitai Resources`, + "ja": `Civitai Resources を表示する` + }, + civitaiResourcesDefault : { + "zh-TW": `圖片預設狀態`, + "en": `Default image state`, + "ja": `画像のデフォルト状態`, + }, + civitaiResourcesImgNumber : { + "zh-TW": `圖片數量`, + "en": `Number of images`, + "ja": `画像の数`, + }, + civitaiResourcesNsfwLevel : { + "zh-TW": `允許 NSFW 圖片`, + "en": `Allow NSFW images`, + "ja": `NSFW 画像を許可する`, + }, }, // 工具列 diff --git a/Www/scss/MainWindow/_MainExif.scss b/Www/scss/MainWindow/_MainExif.scss index 5ef740dc..e1e238cd 100644 --- a/Www/scss/MainWindow/_MainExif.scss +++ b/Www/scss/MainWindow/_MainExif.scss @@ -263,7 +263,7 @@ //資訊 .mainExifList { - padding: 0 5px; + padding-left: 5px; padding-bottom: 20px; overflow-y: auto; overflow-x: hidden; @@ -273,17 +273,28 @@ flex-wrap: wrap; flex-direction: row; align-items: center; - border-top: 1px solid var(--color-blue20); padding: 3px 0; + // 水平線 + &::before { + content: ""; + height: 1px; + background-color: var(--color-blue20); + position: absolute; + top: 0; + left: 0; + right: 5px; + } + // 第一個項目不顯示水平線 + &:first-child::before { + display: none; + } + word-wrap: break-word; //word-break: break-all; font-size: 14px; position: relative; } - .mainExifItem:first-child { - border-top: none; - } // 匯出 .btnExport { @@ -336,8 +347,52 @@ // 允許顯示空白跟換行 white-space: pre-wrap; } + // 不顯示空白跟換行 + .mainExifValue__nowrap { + white-space: normal; + } + + // Civitai resources 的圖片 + .mainExifImgList { + display: none; + + &[active="true"] { + display: flex; + } + + .mainExifImgItem { + height: 150px; + max-width: 150px; + width: 100%; + margin-right: 4px; + //text-align: center; + border-radius: 4px; + + background-size: cover; + background-repeat: no-repeat; + background-position: center; + + overflow: hidden; + img { + width: 100%; + height: 100%; + object-fit: cover; + pointer-events: none; + } + + &:last-child { + margin-right: 0; + } + + &:hover { + cursor: pointer; + outline: 1px solid var(--color-blue40); + outline-offset: -1px; // outline 內縮 + } + } + } - //google map + // google map .mainExifMap { position: relative; width: 100%; @@ -375,7 +430,7 @@ .mainExifBtns { position: absolute; top: 0; - right: -5px; + right: 0; display: flex; flex-direction: row; diff --git a/Www/scss/SettingWindow/SettingWindow.scss b/Www/scss/SettingWindow/SettingWindow.scss index 9c7f050e..6190e233 100644 --- a/Www/scss/SettingWindow/SettingWindow.scss +++ b/Www/scss/SettingWindow/SettingWindow.scss @@ -187,6 +187,18 @@ input::-webkit-inner-spin-button { max-width: 250px; display: flex; } + + .collapseBox { + border-left: 1px solid var(--color-white40); + padding-left: 15px; + padding-bottom: 5px; + margin-left: 15px; + + display: none; + &[active="true"] { + display: block; + } + } } .text-input { @@ -201,7 +213,8 @@ input::-webkit-inner-spin-button { flex: 1; } /******************/ -//擴充套件清單 + +// 擴充套件清單 .pluginLiet { margin-top: 5px; max-width: 800px; @@ -350,7 +363,7 @@ input::-webkit-inner-spin-button { //#region tippy (提示方塊) -//問號 +// 問號 .img-help { display: inline-block; cursor: pointer; @@ -367,7 +380,7 @@ input::-webkit-inner-spin-button { } .tippy-box[data-theme~="tippyMyTheme"] { - //自訂主題 + // 自訂主題 box-sizing: content-box; transition: all 0.15s cubic-bezier(0.49, 0.37, 0.45, 1.4) !important; background-color: var(--color-black); @@ -386,7 +399,7 @@ input::-webkit-inner-spin-button { } //#endregion -//自訂工具列 +// 自訂工具列 .toolbarList { margin-left: 30px; @@ -494,7 +507,7 @@ input::-webkit-inner-spin-button { //--------------------- -//如果是商店APP版,就隱藏區塊 +// 如果是商店APP版,就隱藏區塊 body[showType="storeApp"] [show-not="storeApp"] { display: none; } diff --git a/Www/ts/Config.ts b/Www/ts/Config.ts index 0450522b..275dfacd 100644 --- a/Www/ts/Config.ts +++ b/Www/ts/Config.ts @@ -160,7 +160,7 @@ class Config { public settings = { theme: { /** 是否啟用毛玻璃 */ - "aeroType": "none", //none / win7 / win10 + "aeroType": "none", // none / win7 / win10 /** 視窗縮放比例 */ "zoomFactor": 1.0, /** 文字粗細 */ @@ -204,6 +204,12 @@ class Config { /** 佈局 */ layout: { + + /** 啟用 工具列 */ + mainToolbarEnabled: true, + /** 工具列對齊。 [lef, center] */ + mainToolbarAlign: "left", + /** 啟用 檔案預覽視窗 */ fileListEnabled: true, /** 顯示編號 */ @@ -225,7 +231,7 @@ class Config { dirListImgNumber: 3, /** 啟用 詳細資料視窗 */ - mainExifEnabled: false, + mainExifEnabled: true, /** 寬度 */ mainExifShowWidth: 150, /** 顯示的最大行數 */ @@ -239,10 +245,14 @@ class Config { /** 啟用 相關檔案 */ relatedFilesEnabled: true, - /** 啟用 工具列 */ - mainToolbarEnabled: true, - /** 工具列對齊。 [lef, center] */ - mainToolbarAlign: "left", + /** 啓用 Civitai Resources */ + civitaiResourcesEnabled: true, + /** 圖片預設狀態 true=展開、false=折疊 */ + civitaiResourcesDefault: true, + /** 圖片數量 */ + civitaiResourcesImgNumber: 2, + /** 允許 NSFW 圖片 */ + civitaiResourcesNsfwLevel: 3, /** 大型切換按鈕。 [leftRight, bottom, none] */ largeBtn: "bottom", @@ -368,7 +378,7 @@ class Config { /** 其他 */ other: { /** 開啟 RAW 圖片時,顯示內嵌的預覽圖 */ - rawImageThumbnail : true, + rawImageThumbnail: true, /** 刪除前顯示詢問視窗 */ fileDeletingShowCheckMsg: true, /** 偵測到檔案新增時,插入於。 [auto, start, end] */ @@ -440,7 +450,7 @@ class Config { { ext: "tiff", type: "vips", vipsType: "tif" }, { ext: "dds", type: "vips", vipsType: "wpf,magick" }, { ext: "jxr", type: "vips", vipsType: "wpf" }, - + { ext: "psd", type: "vips", vipsType: "magick" }, { ext: "psb", type: "vips", vipsType: "magick" }, { ext: "pcx", type: "vips", vipsType: "magick" }, diff --git a/Www/ts/Lib.ts b/Www/ts/Lib.ts index ab01949a..5880c716 100644 --- a/Www/ts/Lib.ts +++ b/Www/ts/Lib.ts @@ -374,18 +374,24 @@ class Lib { /** * 送出 GET 的 http 請求 */ - public static async sendGet(type: ("text" | "json" | "base64"), url: string) { + public static async sendGet(type: ("text" | "json" | "base64"), url: string, timeout = 30000) { + + const controller = new AbortController(); + + const timeoutId = setTimeout(() => controller.abort(), timeout); if (type === "text") { let txt = ""; await fetch(url, { "method": "get", + signal: controller.signal, }).then((response) => { return response.text(); }).then((html) => { txt = html; }).catch((err) => { console.log("error: ", err); + throw err; }); return txt; } @@ -394,12 +400,14 @@ class Lib { let json: any = {}; await fetch(url, { "method": "get", + signal: controller.signal, }).then((response) => { return response.json(); }).then((html) => { json = html; }).catch((err) => { console.log("error: ", err); + throw err; }); return json; } @@ -472,7 +480,7 @@ class Lib { * @param domTitle 標題區塊 * @param type "init-true" | "init-false" | "toggle" */ - public static async collapse(domBox: HTMLElement, type: string, funcChange?: (type: string) => void) { + public static async collapse(domBox: HTMLElement, type: string, funcChange?: (type: boolean) => void) { let domContent = domBox.querySelector(".collapse-content") as HTMLElement; if (domContent === null) { return; } @@ -481,7 +489,7 @@ class Lib { if (div === null) { return; } if (funcChange === undefined) { - funcChange = (type: string) => { }; + funcChange = (type: boolean) => { }; } // 自動 @@ -500,7 +508,7 @@ class Lib { } domContent.style.maxHeight = 0 + "px"; domBox.setAttribute("open", "false"); - funcChange(type); + funcChange(false); } if (type === "true") { @@ -513,19 +521,19 @@ class Lib { domContent.style.maxHeight = ""; } }, 300); - funcChange(type); + funcChange(true); } // 無動畫,用於初始化 if (type === "init-false") { domContent.style.maxHeight = 0 + "px"; domBox.setAttribute("open", "false"); - funcChange("false"); + funcChange(false); } if (type === "init-true") { domContent.style.maxHeight = ""; domBox.setAttribute("open", "true"); - funcChange("true"); + funcChange(true); } } @@ -775,3 +783,65 @@ class Throttle { }, timeout); } } + +/** + * 限制最大同時連線數。Chrome最大連線數為6 + */ +class RequestLimiter { + private queue: [HTMLImageElement, string][]; + private inProgress: number; + private maxRequests: number; + + constructor(maxRequests: number) { + this.queue = []; + this.inProgress = 0; + this.maxRequests = maxRequests; + } + + public addRequest(img: HTMLImageElement, url: string) { + + // 檢查 img 元素是否仍然存在於文檔中 + if (!document.body.contains(img)) { + return; + } + + // 檢查佇列中是否已經存在相同的 img 元素和網址 + const index = this.queue.findIndex(([i, u]) => i === img && u === url); + if (index !== -1) { // 如果存在,則忽略這個請求 + return; + } + + // 檢查佇列中是否存在相同的 img 元素但不同的網址 + const index2 = this.queue.findIndex(([i, u]) => i === img && u !== url); + if (index2 !== -1) { // 如果存在,則將舊的請求從佇列中移除 + this.queue.splice(index2, 1); + } + + // 添加新的請求 + this.queue.push([img, url]); + this.processQueue(); + } + + private processQueue() { + while (this.inProgress < this.maxRequests && this.queue.length > 0) { + this.inProgress++; + const [img, url] = this.queue.shift()!; + this.loadImage(img, url).then(() => { + this.inProgress--; + this.processQueue(); + }); + } + } + + private loadImage(img: HTMLImageElement, url: string) { + return new Promise((resolve) => { + if (!document.body.contains(img)) { // 檢查 img 元素是否仍然存在於文檔中 + resolve(); + return; + } + img.addEventListener("load", () => resolve(), { once: true }); + img.addEventListener("error", () => resolve(), { once: true }); + img.src = url; + }); + } +} diff --git a/Www/ts/MainWindow/AiDrawingPrompt.ts b/Www/ts/MainWindow/AiDrawingPrompt.ts index ffdc9722..056180ab 100644 --- a/Www/ts/MainWindow/AiDrawingPrompt.ts +++ b/Www/ts/MainWindow/AiDrawingPrompt.ts @@ -464,12 +464,14 @@ class AiDrawingPrompt { let loraName = intputs["lora_name_" + i]; if (loraName === undefined) { break; } + if (modelStr === 0 && clipStr === 0) { continue; } // 如果都是 0,則略過 if (loraName === "None" || loraName === "none") { continue; } // 如果是 None,則略過 let jsonFormat = Lib.stringifyWithNewlines({ "Model Strength": modelStr, "Clip Strength": clipStr }, true, true); arLora.push({ title: loraName, text: jsonFormat }); + } } @@ -510,6 +512,7 @@ class AiDrawingPrompt { let clipWeight = intputs["clip_weight_" + i]; if (clipWeight === undefined) { break; } + if (modelWeight === 0 && clipWeight === 0) { continue; } // 如果都是 0,則略過 if (loraName === "None" || loraName === "none") { continue; } // 如果是 None,則略過 let jsonFormat = Lib.stringifyWithNewlines({ "Model Strength": modelWeight, @@ -540,6 +543,9 @@ class AiDrawingPrompt { let value = intputs[key]; let type = typeof value; if (type === "number" || type === "string") { + + if (value === 0) { return; } // 如果是 0,則略過 + if (key === "strength_model" || key === "lora_model_strength") { arStrength["Model Strength"] = value; } diff --git a/Www/ts/MainWindow/BulkView.ts b/Www/ts/MainWindow/BulkView.ts index 3edcf2f7..09b70ba9 100644 --- a/Www/ts/MainWindow/BulkView.ts +++ b/Www/ts/MainWindow/BulkView.ts @@ -1320,65 +1320,3 @@ class BulkView { } } - -/** - * 限制最大同時連線數。Chrome最大連線數為6 - */ -class RequestLimiter { - private queue: [HTMLImageElement, string][]; - private inProgress: number; - private maxRequests: number; - - constructor(maxRequests: number) { - this.queue = []; - this.inProgress = 0; - this.maxRequests = maxRequests; - } - - addRequest(img: HTMLImageElement, url: string) { - - // 檢查 img 元素是否仍然存在於文檔中 - if (!document.body.contains(img)) { - return; - } - - // 檢查佇列中是否已經存在相同的 img 元素和網址 - const index = this.queue.findIndex(([i, u]) => i === img && u === url); - if (index !== -1) { // 如果存在,則忽略這個請求 - return; - } - - // 檢查佇列中是否存在相同的 img 元素但不同的網址 - const index2 = this.queue.findIndex(([i, u]) => i === img && u !== url); - if (index2 !== -1) { // 如果存在,則將舊的請求從佇列中移除 - this.queue.splice(index2, 1); - } - - // 添加新的請求 - this.queue.push([img, url]); - this.processQueue(); - } - - private processQueue() { - while (this.inProgress < this.maxRequests && this.queue.length > 0) { - this.inProgress++; - const [img, url] = this.queue.shift()!; - this.loadImage(img, url).then(() => { - this.inProgress--; - this.processQueue(); - }); - } - } - - private loadImage(img: HTMLImageElement, url: string) { - return new Promise((resolve) => { - if (!document.body.contains(img)) { // 檢查 img 元素是否仍然存在於文檔中 - resolve(); - return; - } - img.addEventListener("load", () => resolve(), { once: true }); - img.addEventListener("error", () => resolve(), { once: true }); - img.src = url; - }); - } -} diff --git a/Www/ts/MainWindow/IndexedDBManager.ts b/Www/ts/MainWindow/IndexedDBManager.ts index ea5a1dd6..af6bf739 100644 --- a/Www/ts/MainWindow/IndexedDBManager.ts +++ b/Www/ts/MainWindow/IndexedDBManager.ts @@ -2,12 +2,17 @@ * 資料表名稱 */ var DbStoreName = { + /** Civitai Resources 的暫存資料 */ civitaiResources: "civitaiResources", + /** 詳細資訊面板內的項目折疊狀態 */ + infoPanelCollapse: "infoPanelCollapse", } class IndexedDBManager { - private storeNames: string[] = [DbStoreName.civitaiResources]; // 資料表名稱 + // 資料表名稱 + private storeNames: string[] = Object.values(DbStoreName); + private dbPromise: Promise; constructor(private dbName: string, private version: number) { this.dbPromise = this.init(); @@ -41,7 +46,7 @@ class IndexedDBManager { public async getData(storeName: string, id: string): Promise { const db = await this.dbPromise; return new Promise((resolve, reject) => { - const transaction = db.transaction([storeName]); + const transaction = db.transaction(storeName); const objectStore = transaction.objectStore(storeName); const request = objectStore.get(id); @@ -61,9 +66,16 @@ class IndexedDBManager { * @param data 必須是物件,且有 id 屬性,例如 {id:123, name:"abc"} */ public async saveData(storeName: string, data: any): Promise { + const db = await this.dbPromise; + const transaction = db.transaction(storeName, "readwrite"); + const objectStore = transaction.objectStore(storeName); + objectStore.put(data); + } + + public async deleteData(storeName: string, id: string): Promise { const db = await this.dbPromise; const transaction = db.transaction([storeName], "readwrite"); const objectStore = transaction.objectStore(storeName); - objectStore.add(data); + objectStore.delete(id); } } diff --git a/Www/ts/MainWindow/MainExif.ts b/Www/ts/MainWindow/MainExif.ts index 77c6a2e1..b002da1c 100644 --- a/Www/ts/MainWindow/MainExif.ts +++ b/Www/ts/MainWindow/MainExif.ts @@ -33,6 +33,9 @@ class MainExif { var isHide = false; // 暫時隱藏 var isEnabled = true; // 啟用 檔案預覽視窗 + /** 請求限制器 */ + const limiter = new RequestLimiter(3); + /** 頁籤的類型 */ var TabType = { /** 資訊 */ @@ -534,7 +537,7 @@ class MainExif { const data = cdata[i].data; // 折疊面板 - let collapseDom = getCollapseDom(node, true); + let collapseDom = await getCollapseDom(node, true); data.forEach(item => { collapseDom.domContent.appendChild(getItemDom(item.title, item.text)); }); @@ -546,14 +549,14 @@ class MainExif { // 把 ComfyScript 放在最下面,並預設展開 if (comfyScript !== undefined) { // 折疊面板 - let collapseDom = getCollapseDom("ComfyScript", true); + let collapseDom = await getCollapseDom("ComfyScript", true); collapseDom.domContent.appendChild(getItemDom("ComfyScript", comfyScript)); domTabContentInfo.appendChild(collapseDom.domBox); } // 把 ComfyUI 的原始資料放在最下面,並預設折疊 if (comfyuiPrompt !== undefined || comfyuiWorkflow !== undefined) { // 折疊面板 - let collapseDom = getCollapseDom("ComfyUI Data", false); + let collapseDom = await getCollapseDom("ComfyUI Data", false); if (comfyuiGenerationData !== undefined) { let jsonF = Lib.jsonStrFormat(comfyuiGenerationData); @@ -711,14 +714,14 @@ class MainExif { let tempId: any[] = []; // 折疊面板 - let collapseDom = getCollapseDom("Civitai Resources", true, "civitaiResources"); + let collapseDom = await getCollapseDom("Civitai Resources", true, "civitaiResources", (type) => { }); for (let i = 0; i < data.length; i++) { const item = data[i]; let hash = item.hash; let modelVersionId = item.modelVersionId; let dbKey = modelVersionId || hash; - if (hash === undefined && modelVersionId === undefined) { + if (dbKey === undefined || dbKey === null) { console.warn("Civitai 未預期的格式"); console.warn(item); continue; @@ -728,15 +731,31 @@ class MainExif { let modelType: any = item.type; let modelName: any; let name: any; + let images: any[]; let error: any; // 先嘗試從 indexedDB 取得資料 let dbData = await M.db?.getData(DbStoreName.civitaiResources, dbKey); + + if (dbData !== undefined) { + // 如果發生過錯誤,且時間超過 1 天,就重新下載 + if (dbData.error !== undefined) { + let time = new Date(dbData.time).getTime(); + let timeNow = new Date().getTime(); + //if (isNaN(time) || timeNow - time > 20 * 1000) { + if (isNaN(time) || timeNow - time > 24 * 60 * 60 * 1000) { + dbData = undefined; + } + } + } + if (dbData !== undefined) { + modelId = dbData.modelId; modelType = dbData.modelType; modelName = dbData.modelName; name = dbData.name; + images = dbData.images; modelVersionId = dbData.modelVersionId; error = dbData.error; // console.log("indexedDB 取得資料", modelId, modelName, error); @@ -750,46 +769,75 @@ class MainExif { if (path !== fileInfo2.FullPath) { return; } // 曾經下載過,且資料是 error,就不再載入 - if (error) { continue; } + if (error === "Model not found") { continue; } // 先產生一個空的 dom 項目,待資料載入完畢後,再替換 let oldDom = getItemDom(dbKey, "Loading" + "\n" + "-"); collapseDom.domContent.appendChild(oldDom); + let ompleteCount = 0; + // 項目完成時呼叫的函數,用與判斷是否全部都已經完成 + let completeFunc = () => { + ompleteCount++; + if (ompleteCount === data.length) { + // 如果移除項目後,折疊面板沒有任何項目,則移除折疊面板 + if (collapseDom.domContent.children.length === 0) { + collapseDom.domBox.parentNode?.removeChild(collapseDom.domBox); + } + } + } + setTimeout(async () => { // 如果 indexedDB 沒有資料,則從 Civitai 取得資料 if (error === undefined && - (modelId === undefined || modelName === undefined)) { - let url: string; - let result - if (hash) { + (modelId === undefined || modelName === undefined || images === undefined)) { + let url: string = ""; + let result; + let timeout = 10 * 1000; - url = `https://civitai.com/api/v1/model-versions/by-hash/` + hash; - result = await Lib.sendGet("json", url); + try { + if (hash) { - // 如果 hash 超過 10 個字,且找不到資源,則只取前 10 個字再試一次 - if (result.error && hash.length > 10) { - hash = hash.substring(0, 10); url = `https://civitai.com/api/v1/model-versions/by-hash/` + hash; - result = await Lib.sendGet("json", url); - console.log("Civitai 重新請求資料", url); + result = await Lib.sendGet("json", url, timeout); + + // 如果 hash 超過 10 個字,且找不到資源,則只取前 10 個字再試一次 + if (result.error && hash.length > 10) { + hash = hash.substring(0, 10); + url = `https://civitai.com/api/v1/model-versions/by-hash/` + hash; + result = await Lib.sendGet("json", url, timeout); + // console.log("Civitai 重新請求資料", url); + } + } else { + url = `https://civitai.com/api/v1/model-versions/` + modelVersionId; + result = await Lib.sendGet("json", url, timeout); + } + } catch (e) { + + console.error("Civitai 請求失敗", e); + let exifValue = oldDom.querySelector(".mainExifValue") as HTMLElement + exifValue.innerHTML = "Error"; + result = { + error: "Request error" } - } else { - url = `https://civitai.com/api/v1/model-versions/` + modelVersionId; - result = await Lib.sendGet("json", url); } + // console.log("Civitai 請求資料", url); modelId = result.modelId; modelName = result.model?.name; modelType = result.model?.type; name = result.name; + images = result.images; modelVersionId = result.id; error = result.error; if (result.error) { - console.warn("Civitai 返回 error", url, result); + console.log("Civitai 返回 error", url, result); + let exifValue = oldDom.querySelector(".mainExifValue") as HTMLElement + exifValue.innerHTML = "Error"; + // 如果找不到資源,則一樣存到 indexedDB M.db?.saveData(DbStoreName.civitaiResources, { id: dbKey, @@ -798,10 +846,18 @@ class MainExif { }); } else { if (modelId === undefined || modelName === undefined) { - console.warn("Civitai 返回資料解析失敗", url, result); + console.log("Civitai 返回資料解析失敗", url, result); } + // 存到 indexedDB - M.db?.saveData(DbStoreName.civitaiResources, { + let images2 = result.images.map((item: any) => { + return { + url: item.url, + nsfwLevel: item.nsfwLevel, + type: item.type + }; + }); + await M.db?.saveData(DbStoreName.civitaiResources, { id: dbKey, time: new Date().format("yyyy-MM-dd hh:mm:ss"), modelId: modelId, @@ -809,16 +865,16 @@ class MainExif { modelName: modelName, modelType: modelType, name: name, + images: images2 }); + console.log("Civitai 成功", url); + } // 重複的項目 - if (tempId.includes(modelVersionId)) { + if (modelVersionId !== undefined && tempId.includes(modelVersionId)) { oldDom.parentNode?.removeChild(oldDom); - // 如果移除項目後,折疊面板沒有任何項目,則移除折疊面板 - if (collapseDom.domContent.children.length === 0) { - collapseDom.domBox.parentNode?.removeChild(collapseDom.domBox); - } + completeFunc(); return; } tempId.push(modelVersionId); @@ -826,43 +882,158 @@ class MainExif { if (error) { oldDom.parentNode?.removeChild(oldDom); - // 如果移除項目後,折疊面板沒有任何項目,則移除折疊面板 - if (collapseDom.domContent.children.length === 0) { - collapseDom.domBox.parentNode?.removeChild(collapseDom.domBox); - } + completeFunc(); return; } if (modelId === undefined || modelName === undefined) { + completeFunc(); return; } // 檢查 oldDom 是否還存在 if (oldDom.parentNode !== null) { + if (baseWindow.appInfo === undefined) { return; } + // 產生新的 dom let newItem = Lib.newDom(` -
-
${modelType}
-
${modelName}
${name}
-
-
${SvgList["tool-civitai.svg"]}
-
-
`); +
+
${Lib.escape(modelType)}
+
+ ${Lib.escape(modelName)}
+ ${Lib.escape(name)} +
+
+
+
+
${SvgList["expand.svg"]}
+
${SvgList["collapse.svg"]}
+
${SvgList["tool-civitai.svg"]}
+
+
`); + domTabContentInfo.appendChild(newItem); + + let btnExpand = newItem.querySelector(".mainExifBtnExpand") as HTMLElement; // 折疊 + let btnCollapse = newItem.querySelector(".mainExifBtnCollapse") as HTMLElement; // 折疊 let btnCivitai = newItem.querySelector(".mainExifBtnCivitai") as HTMLElement; + let divImgList = newItem.querySelector(".mainExifImgList") as HTMLElement; + + // 產生預覽圖 + let imgCount = M.config.settings.layout.civitaiResourcesImgNumber; + let nsfwLevel = M.config.settings.layout.civitaiResourcesNsfwLevel; + + // 判斷是否有圖片 + let isHaveImg = false; + // 載入圖片 + let funcLoadImg: (() => void)[] = []; + + for (let i = 0; i < images.length; i++) { + const item = images[i]; + if (item.type === "image" && item.nsfwLevel <= nsfwLevel) { + + isHaveImg = true; + + // 開啟網址時下載圖片,並回傳其 icon + let name = `Civitai\\${dbKey}-${i + 1}.jpg`; + let imgPath = Lib.Combine([baseWindow.appInfo.tempDirWebFile, name]); + let imgUrl = WebAPI.Img.webIcon(item.url, name); + + let imgItem = Lib.newDom(` +
+ +
+ `); + divImgList.appendChild(imgItem); + imgItem.setAttribute("data-path", imgPath); + + const domImg = imgItem.querySelector("img") as HTMLImageElement; + + // 展開時,載入圖片,已經載入過的就不再載入 + let isLoad = false; + funcLoadImg.push(() => { + if (isLoad) { return; } + isLoad = true; + limiter.addRequest(domImg, imgUrl); // 發出請求 + }) + + // 圖片載入失敗時,顯示錯誤圖示 + domImg.onerror = () => { + domImg.src = "./img/error.svg"; + domImg.style.objectFit = "contain"; + } + + // 雙擊左鍵 + Lib.addEventDblclick(imgItem, async (e: MouseEvent) => { // 圖片物件 + let imgPath = imgItem.getAttribute("data-path"); + if (imgPath === null) { return; } + M.script.open.openNewWindow(imgPath); + }); + + imgCount--; + if (imgCount <= 0) { break; } + } + } + btnCivitai.addEventListener("click", async () => { let url = "https://civitai.com/models/" + modelId + "?modelVersionId=" + modelVersionId; WV_RunApp.OpenUrl(url); }); - domTabContentInfo.appendChild(newItem); + + // 套用狀態。 true=展開 、 false=折疊、 null=不顯示 + function setCollapse(t: boolean | null) { + if (t) { // 狀態是展開,顯示折疊按鈕 + btnExpand.style.display = "none"; + btnCollapse.style.display = ""; + divImgList.setAttribute("active", "true"); + + // 展開時,載入圖片 + funcLoadImg.forEach(func => { + func(); + }) + + } else if (t === false) { + btnExpand.style.display = ""; + btnCollapse.style.display = "none"; + divImgList.setAttribute("active", ""); + } else { + // 什麼都不顯示 + btnExpand.style.display = "none"; + btnCollapse.style.display = "none"; + divImgList.removeAttribute("active"); + } + } + btnExpand.addEventListener("click", async () => { // 展開 + setCollapse(true); + // 儲存折疊狀態 + M.db?.saveData(DbStoreName.infoPanelCollapse, { id: "civitaiImg-" + dbKey, collapse: true }); + }); + btnCollapse.addEventListener("click", async () => { // 折疊 + setCollapse(false); + // 儲存折疊狀態 + M.db?.saveData(DbStoreName.infoPanelCollapse, { id: "civitaiImg-" + dbKey, collapse: false }); + }); + + // 從 db 讀取折疊狀態 + let dbCollapse = await M.db?.getData(DbStoreName.infoPanelCollapse, "civitaiImg-" + dbKey); + if (dbCollapse !== undefined) { + setCollapse(dbCollapse.collapse); + } else { + setCollapse(M.config.settings.layout.civitaiResourcesDefault); + } + // 如果沒有圖片,就不顯示展開按鈕 + if (isHaveImg === false) { + setCollapse(null); + } // 把新的 dom 插到原有的 dom 後面,然後刪除原有的 dom oldDom.insertAdjacentElement("afterend", newItem); oldDom.parentNode?.removeChild(oldDom); + + completeFunc(); } }, 1); - } if (collapseDom.domContent.children.length > 0) { @@ -1090,8 +1261,8 @@ class MainExif { } Lib.collapse(domBox, "toggle", (type) => { //切換折疊狀態 - let t = (type === "true"); - M.config.settings.layout.mainExifCollapse[title] = t; //更改狀態後,儲存折疊狀態 + + M.config.settings.layout.mainExifCollapse[title] = type; //更改狀態後,儲存折疊狀態 }); }) @@ -1104,11 +1275,11 @@ class MainExif { * @param type 初始狀態 * @param key 儲存設定值的key */ - function getCollapseDom(title: string, type: boolean, key?: string) { + async function getCollapseDom(title: string, type: boolean, key?: string, funcChange?: (type: boolean) => void) { // 外框物件 let domBox = Lib.newDom(` -
+
`); @@ -1133,10 +1304,14 @@ class MainExif { // 從設定讀取折疊狀態 let open; + // 從 db 讀取折疊狀態 if (key !== undefined) { - open = M.config.settings.layout.mainExifCollapse[key]; + let dbCollapse = await M.db?.getData(DbStoreName.infoPanelCollapse, key); + if (dbCollapse !== undefined) { open = dbCollapse.collapse; } + } + if (open === undefined) { + open = type; } - if (open === undefined) { open = type; } // 初始化 折疊面板 Lib.collapse(domBox, "init-" + open); // 不使用動畫直接初始化狀態 @@ -1146,12 +1321,12 @@ class MainExif { if (target !== null && target.classList.contains("mainExifRelatedTitleBtn")) { return; } - Lib.collapse(domBox, "toggle", (type) => { + Lib.collapse(domBox, "toggle", async (type) => { if (key !== undefined) { - let t = (type === "true"); - M.config.settings.layout.mainExifCollapse[key] = t; // 更改狀態後,儲存折疊狀態 + funcChange?.(type); + // 更改狀態後,儲存折疊狀態 + await M.db?.saveData(DbStoreName.infoPanelCollapse, { id: key, collapse: type }); } - }); // 切換折疊狀態 }); diff --git a/Www/ts/MainWindow/MainWindow.ts b/Www/ts/MainWindow/MainWindow.ts index 8b46c42e..9c878466 100644 --- a/Www/ts/MainWindow/MainWindow.ts +++ b/Www/ts/MainWindow/MainWindow.ts @@ -95,7 +95,7 @@ class MainWindow { (async () => { - db = await new IndexedDBManager("tiefseeDB", 1); + db = await new IndexedDBManager("tiefseeDB", 2); this.db = db; fileShow.openNone(); // 不顯示任何東西 diff --git a/Www/ts/SettingWindow/SettingWindow.ts b/Www/ts/SettingWindow/SettingWindow.ts index 8736588c..47447023 100644 --- a/Www/ts/SettingWindow/SettingWindow.ts +++ b/Www/ts/SettingWindow/SettingWindow.ts @@ -898,7 +898,7 @@ class SettingWindow { addLoadEvent(() => { // 顯示 詳細資料面板 var switch_mainExifEnabled = getDom("#switch-mainExifEnabled") as HTMLInputElement; - switch_mainExifEnabled.checked = config.settings["layout"]["mainExifEnabled"]; // + switch_mainExifEnabled.checked = config.settings["layout"]["mainExifEnabled"]; switch_mainExifEnabled.addEventListener("change", () => { let val = switch_mainExifEnabled.checked; config.settings["layout"]["mainExifEnabled"] = val; @@ -935,6 +935,52 @@ class SettingWindow { appleSettingOfMain(); }); + // 顯示 Civitai Resources + var divCivitaiBox = getDom("#civitaiBox") as HTMLElement; + function updateCivitaiBox() { + if (switch_civitaiResourcesEnabled.checked) { + divCivitaiBox.setAttribute("active", "true"); + } else { + divCivitaiBox.removeAttribute("active"); + } + } + var switch_civitaiResourcesEnabled = getDom("#switch-civitaiResourcesEnabled") as HTMLInputElement; + switch_civitaiResourcesEnabled.checked = config.settings["layout"]["civitaiResourcesEnabled"]; + updateCivitaiBox(); + switch_civitaiResourcesEnabled.addEventListener("change", () => { + let val = switch_civitaiResourcesEnabled.checked; + config.settings["layout"]["civitaiResourcesEnabled"] = val; + appleSettingOfMain(); + updateCivitaiBox(); + }); + + // 圖片預設狀態 + var select_civitaiResourcesDefault = getDom("#select-civitaiResourcesDefault") as HTMLSelectElement; + select_civitaiResourcesDefault.value = config.settings.layout.civitaiResourcesDefault.toString(); + select_civitaiResourcesDefault.addEventListener("change", () => { + let val = select_civitaiResourcesDefault.value; + config.settings.layout.civitaiResourcesDefault = val === "true"; + appleSettingOfMain(); + }); + + // 圖片數量 + var select_civitaiResourcesImgNumber = getDom("#select-civitaiResourcesImgNumber") as HTMLSelectElement; + select_civitaiResourcesImgNumber.value = config.settings.layout.civitaiResourcesImgNumber.toString(); + select_civitaiResourcesImgNumber.addEventListener("change", () => { + let val = Number(select_civitaiResourcesImgNumber.value); + config.settings.layout.civitaiResourcesImgNumber = val; + appleSettingOfMain(); + }); + + // 允許 NSFW 圖片 + var switch_civitaiResourcesNsfwLevel = getDom("#switch-civitaiResourcesNsfwLevel") as HTMLInputElement; + switch_civitaiResourcesNsfwLevel.checked = config.settings.layout.civitaiResourcesNsfwLevel == 99; + switch_civitaiResourcesNsfwLevel.addEventListener("change", () => { + let val = switch_civitaiResourcesNsfwLevel.checked; + config.settings.layout.civitaiResourcesNsfwLevel = val ? 99 : 3; + appleSettingOfMain(); + }); + }) // 大型切換按鈕 diff --git a/Www/ts/WebAPI.ts b/Www/ts/WebAPI.ts index 48f123f6..4652fcc3 100644 --- a/Www/ts/WebAPI.ts +++ b/Www/ts/WebAPI.ts @@ -85,6 +85,16 @@ class WebAPI { return APIURL + "/api/getFileIcon?size=256&path=" + encodeURIComponent(path); } + /** + * 從網路下載圖片後,返回圖片的 icon + */ + static webIcon(url: string, path: string) { + let encodePath = encodeURIComponent(path); + let encodeUrl = encodeURIComponent(url); + let r = 0; // Math.random(); // 避免快取 + return APIURL + `/api/getWebIcon?size=256&url=${encodeUrl}&path=${encodePath}&r=${r}`; + } + /** * 取得圖片網址 */ diff --git a/Www/ts/d/NetAPI.d.ts b/Www/ts/d/NetAPI.d.ts index 6a0eb3c3..9b293f03 100644 --- a/Www/ts/d/NetAPI.d.ts +++ b/Www/ts/d/NetAPI.d.ts @@ -1,6 +1,6 @@ interface WebWindow { - /** 運行js */ + /** 運行 js */ RunJs(js: string): string; /** 視窗取得焦點 */ @@ -41,13 +41,13 @@ interface WebWindow { interface WV_Window { - /** 清理webview2的暫存 */ + /** 清理 webview2 的暫存 */ ClearBrowserCache(): void; /** 儲存到 start.ini */ SetStartIni(startPort: number, startType: number) - /** 取得 AppInfo*/ + /** 取得 AppInfo */ GetAppInfo(): string; /** 網頁載入完成後,呼叫此函數才會顯示視窗 */ @@ -79,10 +79,10 @@ interface WV_Window { /** 傳入 webWindow,將其設為目前視窗的子視窗*/ SetOwner(webwindow: WebWindow); - /** 在父親視窗運行js */ + /** 在父視窗執行 js */ RunJsOfParent(js: string): string; - /** 啟用AERO毛玻璃效果 */ + /** 啟用 AERO 毛玻璃效果 */ SetAERO(type: ("win7" | "win10")): void; /** 設定縮放倍率,預設 1.0 */ @@ -547,6 +547,9 @@ interface AppInfo { /** AppData(使用者資料) */ appDataPath: string; + /** 暫存資料夾 - 從網路下載的檔案 */ + tempDirWebFile: string; + /** 目前使用的port */ mainPort: number; @@ -583,4 +586,4 @@ interface FileWatcherData { OldFullPath: string; ChangeType: "changed" | "created" | "deleted" | "renamed"; FileType: "file" | "dir"; -} \ No newline at end of file +}