diff --git a/.gitignore b/.gitignore index 43c32504..e628117a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ dist-ssr .venv # Customize -/build \ No newline at end of file +/build +error.txt \ No newline at end of file diff --git a/locales/en.yml b/locales/en.yml index a52ac107..f72907ac 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -243,4 +243,31 @@ config_page: open_config_filepath: label: "Open Config File" switch_compute_device: - label: "Switch VRCT To CPU/GPU Version" \ No newline at end of file + label: "Switch VRCT To CPU/GPU Version" + section_label_plugins: Plugins # Exception, It'll be moved later. + + plugins: + downloaded_version: "Downloaded version: {{downloaded_version}}" + latest_version: "Latest version: {{latest_version}}" + available_after_updating: "Available after updating to the latest version" + unavailable_downloaded: "Currently unavailable due to incompatibility with the VRCT version in use" + no_latest_info: "Unable to retrieve the latest information" + using_latest_version: "Using the latest version" + available_latest_version: "Latest version available" + unavailable_latest_version: "Latest version currently unavailable" + available_in_latest_vrct_version: "Available in the latest VRCT version" + unavailable_not_downloaded: "Currently unavailable" + +plugin_notifications: + downloading: Downloading the plugin. + downloaded_success: Downloaded successfully. + downloaded_error: Download failed. + + updating: Updating the plugin. + updated_success: Updated successfully. + updated_error: Update failed. + + disabled_out_of_support: The plugin has been disabled. It's not supported on this VRCT version. + + is_enabled: The plugin has enabled. + is_disabled: The plugin has disabled. \ No newline at end of file diff --git a/locales/ja.yml b/locales/ja.yml index 188c5325..3d109b8a 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -243,4 +243,31 @@ config_page: open_config_filepath: label: "設定ファイルを開く" switch_compute_device: - label: "VRCT CPU/GPUバージョンの切り替え" \ No newline at end of file + label: "VRCT CPU/GPUバージョンの切り替え" + section_label_plugins: プラグイン # Exception, It'll be moved later. + + plugins: + downloaded_version: "ダウンロード済バージョン: {{downloaded_version}}" + latest_version: "最新バージョン: {{latest_version}}" + available_after_updating: 最新版にアップデート後 利用可能 + unavailable_downloaded: 現在利用不可 使用中VRCTバージョンとの互換性なし + no_latest_info: 最新情報が取得できません + using_latest_version: 最新版を使用中 + available_latest_version: 最新版を利用可能 + unavailable_latest_version: 最新版は現在利用不可 + available_in_latest_vrct_version: VRCT最新版で利用可能 + unavailable_not_downloaded: 現在利用不可 + +plugin_notifications: + downloading: プラグインをダウンロード中。 + downloaded_success: プラグインのダウンロードが完了しました。 + downloaded_error: プラグインのダウンロードに失敗しました。 + + updating: プラグインをアップデート中。 + updated_success: プラグインのアップデートが完了しました。 + updated_error: プラグインのアップデートに失敗しました。 + + disabled_out_of_support: 現在のバージョンとの互換性がありません。プラグインを無効にしました。 + + is_enabled: プラグインを有効にしました。 + is_disabled: プラグインを無効にしました。 \ No newline at end of file diff --git a/map-stack.js b/map-stack.js new file mode 100644 index 00000000..9deb67f1 --- /dev/null +++ b/map-stack.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// 使用例: node map-stack.js +// カレントディレクトリの error.txt を読み込み、各エラーフレームから +// 対応するソースマップ(dist/assets/ 以下の *.js.map)を用いて元の位置情報を出力する + +import fs from "fs"; +import path from "path"; +import { SourceMapConsumer } from "source-map"; + +// 各スタックフレームにマッチする正規表現 +const FRAME_REGEX = /^\s*at\s+(.*?)\s+\((.*):(\d+):(\d+)\)$/; + +// スタックトレースのパース関数 +const parseStackTrace = (text) => { + return text + .split(/\r?\n/) + .map((line) => { + const match = line.match(FRAME_REGEX); + if (match) { + return { + original: line, + functionName: match[1], + file: match[2], + line: Number(match[3]), + column: Number(match[4]) + }; + } else { + return { original: line }; + } + }); +}; + +// エラースタックをソースマップから逆引きする関数(位置情報のみを表示) +const mapStackTrace = async () => { + const errorTxtPath = path.resolve(process.cwd(), "error.txt"); + const stackTraceText = fs.readFileSync(errorTxtPath, "utf8"); + const frames = parseStackTrace(stackTraceText); + + const consumerMap = new Map(); + + const mappedFrames = await Promise.all(frames.map(async (frame) => { + if (frame.file && frame.line && frame.column) { + const relativeFile = frame.file.replace(/^\//, ""); // 例: "assets/main-Td8-sruo.js" + const mapFilePath = path.resolve(process.cwd(), "dist", relativeFile + ".map"); + let consumer = consumerMap.get(mapFilePath); + if (!consumer) { + const rawSourceMap = fs.readFileSync(mapFilePath, "utf8"); + consumer = await new SourceMapConsumer(rawSourceMap); + consumerMap.set(mapFilePath, consumer); + } + const pos = consumer.originalPositionFor({ + line: frame.line, + column: frame.column + }); + + if (pos && pos.source && pos.line != null && pos.column != null) { + return ` at ${frame.functionName} (${pos.source}:${pos.line}:${pos.column})`; + } else { + return frame.original; + } + } else { + return frame.original; + } + })); + + consumerMap.forEach((consumer) => consumer.destroy()); + return mappedFrames.join("\n"); +}; + +mapStackTrace() + .then((mapped) => { + console.log("--- Mapped Stack Trace ---"); + console.log(mapped); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/package-lock.json b/package-lock.json index ebda8372..308c262d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "tauri-app", "version": "0.0.0", "dependencies": { + "@babel/standalone": "7.26.9", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@mui/material": "6.2.0", @@ -19,17 +20,21 @@ "i18next": "24.1.0", "jotai": "2.10.3", "js-base64": "3.7.7", - "js-yaml": "4.1.0", + "jszip": "3.10.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-error-boundary": "5.0.0", "react-i18next": "15.2.0", - "react-resizable-layout": "0.7.2" + "react-resizable-layout": "0.7.2", + "semver": "7.7.1" }, "devDependencies": { + "@rollup/plugin-yaml": "^4.1.2", "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.0.3", + "source-map": "0.7.4", + "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } }, @@ -207,23 +212,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -261,9 +266,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -271,14 +276,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/standalone": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.26.9.tgz", + "integrity": "sha512-UTeQKy0kzJwWRe55kT1uK4G9H6D0lS6G4207hCU/bDaOhA5t2aC0qHN6GmID0Axv3OFLNXm27NdqcWp+BXcGtA==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -302,9 +315,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -331,6 +344,14 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@emotion/cache": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", @@ -447,9 +468,9 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ "ppc64" ], @@ -462,9 +483,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ "arm" ], @@ -477,9 +498,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", "cpu": [ "arm64" ], @@ -492,9 +513,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", "cpu": [ "x64" ], @@ -507,9 +528,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -522,9 +543,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", "cpu": [ "x64" ], @@ -537,9 +558,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", "cpu": [ "arm64" ], @@ -552,9 +573,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", "cpu": [ "x64" ], @@ -567,9 +588,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", "cpu": [ "arm" ], @@ -582,9 +603,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", "cpu": [ "arm64" ], @@ -597,9 +618,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", "cpu": [ "ia32" ], @@ -612,9 +633,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", "cpu": [ "loong64" ], @@ -627,9 +648,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", "cpu": [ "mips64el" ], @@ -642,9 +663,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", "cpu": [ "ppc64" ], @@ -657,9 +678,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", "cpu": [ "riscv64" ], @@ -672,9 +693,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", "cpu": [ "s390x" ], @@ -687,9 +708,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -701,10 +722,25 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", "cpu": [ "x64" ], @@ -717,9 +753,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -732,9 +768,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", "cpu": [ "x64" ], @@ -747,9 +783,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", "cpu": [ "x64" ], @@ -762,9 +798,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", "cpu": [ "arm64" ], @@ -777,9 +813,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", "cpu": [ "ia32" ], @@ -792,9 +828,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", "cpu": [ "x64" ], @@ -1185,6 +1221,28 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rollup/plugin-yaml": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-yaml/-/plugin-yaml-4.1.2.tgz", + "integrity": "sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "js-yaml": "^4.1.0", + "tosource": "^2.0.0-alpha.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", @@ -1208,9 +1266,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", + "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", "cpu": [ "arm" ], @@ -1220,9 +1278,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", + "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", "cpu": [ "arm64" ], @@ -1232,9 +1290,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", + "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", "cpu": [ "arm64" ], @@ -1244,9 +1302,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", + "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", "cpu": [ "x64" ], @@ -1255,10 +1313,34 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", + "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", + "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", + "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", "cpu": [ "arm" ], @@ -1268,9 +1350,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", + "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", "cpu": [ "arm" ], @@ -1280,9 +1362,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz", + "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==", "cpu": [ "arm64" ], @@ -1292,9 +1374,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", + "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", "cpu": [ "arm64" ], @@ -1303,10 +1385,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", + "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", + "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", "cpu": [ "ppc64" ], @@ -1316,9 +1410,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", + "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", "cpu": [ "riscv64" ], @@ -1328,9 +1422,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", + "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", "cpu": [ "s390x" ], @@ -1340,9 +1434,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz", + "integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==", "cpu": [ "x64" ], @@ -1352,9 +1446,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz", + "integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==", "cpu": [ "x64" ], @@ -1364,9 +1458,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", + "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", "cpu": [ "arm64" ], @@ -1376,9 +1470,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", + "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", "cpu": [ "ia32" ], @@ -1388,9 +1482,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", + "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", "cpu": [ "x64" ], @@ -2330,6 +2424,11 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -2689,9 +2788,9 @@ } }, "node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -2700,30 +2799,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, "node_modules/escalade": { @@ -3424,6 +3524,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immutable": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", @@ -3933,6 +4038,17 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3953,6 +4069,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4067,9 +4191,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", + "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", "funding": [ { "type": "github", @@ -4364,6 +4488,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4476,9 +4605,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -4494,7 +4623,7 @@ } ], "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4510,6 +4639,11 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4575,6 +4709,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz", + "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-i18next": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz", @@ -4659,6 +4804,25 @@ "node": ">=4" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -4764,9 +4928,9 @@ } }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz", + "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==", "dependencies": { "@types/estree": "1.0.6" }, @@ -4778,22 +4942,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@rollup/rollup-android-arm-eabi": "4.35.0", + "@rollup/rollup-android-arm64": "4.35.0", + "@rollup/rollup-darwin-arm64": "4.35.0", + "@rollup/rollup-darwin-x64": "4.35.0", + "@rollup/rollup-freebsd-arm64": "4.35.0", + "@rollup/rollup-freebsd-x64": "4.35.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", + "@rollup/rollup-linux-arm-musleabihf": "4.35.0", + "@rollup/rollup-linux-arm64-gnu": "4.35.0", + "@rollup/rollup-linux-arm64-musl": "4.35.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", + "@rollup/rollup-linux-riscv64-gnu": "4.35.0", + "@rollup/rollup-linux-s390x-gnu": "4.35.0", + "@rollup/rollup-linux-x64-gnu": "4.35.0", + "@rollup/rollup-linux-x64-musl": "4.35.0", + "@rollup/rollup-win32-arm64-msvc": "4.35.0", + "@rollup/rollup-win32-ia32-msvc": "4.35.0", + "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" } }, @@ -4836,6 +5003,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -4878,10 +5050,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "bin": { "semver": "bin/semver.js" }, @@ -4919,6 +5090,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4975,11 +5151,12 @@ } }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/source-map-js": { @@ -5022,6 +5199,14 @@ "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -5190,6 +5375,15 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, + "node_modules/tosource": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/tosource/-/tosource-2.0.0-alpha.3.tgz", + "integrity": "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5338,6 +5532,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -5349,13 +5548,13 @@ } }, "node_modules/vite": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", - "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dependencies": { - "esbuild": "^0.24.0", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index 48ee7f7e..f9948b13 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "release-all": "npm run release && npm run release-cuda" }, "dependencies": { + "@babel/standalone": "7.26.9", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@mui/material": "6.2.0", @@ -34,17 +35,21 @@ "i18next": "24.1.0", "jotai": "2.10.3", "js-base64": "3.7.7", - "js-yaml": "4.1.0", + "jszip": "3.10.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-error-boundary": "5.0.0", "react-i18next": "15.2.0", - "react-resizable-layout": "0.7.2" + "react-resizable-layout": "0.7.2", + "semver": "7.7.1" }, "devDependencies": { + "@rollup/plugin-yaml": "^4.1.2", "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.0.3", + "source-map": "0.7.4", + "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } } diff --git a/src-python/config.py b/src-python/config.py index df1f12e9..879b90e5 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -555,6 +555,18 @@ class Config: self._HOTKEYS[key] = value self.saveConfig(inspect.currentframe().f_code.co_name, self.HOTKEYS, immediate_save=True) + @property + @json_serializable('PLUGINS_STATUS') + def PLUGINS_STATUS(self): + return self._PLUGINS_STATUS + + @PLUGINS_STATUS.setter + def PLUGINS_STATUS(self, value): + if isinstance(value, list): + if all(isinstance(item, dict) for item in value): + self._PLUGINS_STATUS = value + self.saveConfig(inspect.currentframe().f_code.co_name, self.PLUGINS_STATUS, immediate_save=True) + @property @json_serializable('MIC_AVG_LOGPROB') def MIC_AVG_LOGPROB(self): @@ -1068,6 +1080,7 @@ class Config: "toggle_transcription_send": None, "toggle_transcription_receive": None, } + self._PLUGINS_STATUS = [] self._MIC_AVG_LOGPROB = -0.8 self._MIC_NO_SPEECH_PROB = 0.6 self._AUTO_SPEAKER_SELECT = True diff --git a/src-python/controller.py b/src-python/controller.py index 3cd5ae02..c0f5dabc 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -447,11 +447,11 @@ class Controller: return {"status":200, "result":config.VERSION} def checkSoftwareUpdated(self) -> dict: - update_flag = model.checkSoftwareUpdated() + software_update_info = model.checkSoftwareUpdated() self.run( 200, - self.run_mapping["update_software_flag"], - update_flag, + self.run_mapping["software_update_info"], + software_update_info, ) @staticmethod @@ -1061,6 +1061,15 @@ class Controller: config.HOTKEYS = data return {"status":200, "result":config.HOTKEYS} + @staticmethod + def getPluginsStatus(*args, **kwargs) -> dict: + return {"status":200, "result":config.PLUGINS_STATUS} + + @staticmethod + def setPluginsStatus(data, *args, **kwargs) -> dict: + config.PLUGINS_STATUS = data + return {"status":200, "result":config.PLUGINS_STATUS} + @staticmethod def getSpeakerAvgLogprob(*args, **kwargs) -> dict: return {"status":200, "result":config.SPEAKER_AVG_LOGPROB} diff --git a/src-python/mainloop.py b/src-python/mainloop.py index cd50a503..5c5fb744 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -38,7 +38,7 @@ run_mapping = { "mic_device_list":"/run/mic_device_list", "speaker_device_list":"/run/speaker_device_list", - "update_software_flag":"/run/update_software_flag", + "software_update_info":"/run/software_update_info", "initialization_progress":"/run/initialization_progress", "initialization_complete":"/run/initialization_complete", @@ -190,6 +190,9 @@ mapping = { "/get/data/hotkeys": {"status": True, "variable":controller.getHotkeys}, "/set/data/hotkeys": {"status": True, "variable":controller.setHotkeys}, + "/get/data/plugins_status": {"status": True, "variable":controller.getPluginsStatus}, + "/set/data/plugins_status": {"status": True, "variable":controller.setPluginsStatus}, + "/get/data/mic_avg_logprob": {"status": True, "variable":controller.getMicAvgLogprob}, "/set/data/mic_avg_logprob": {"status": True, "variable":controller.setMicAvgLogprob}, diff --git a/src-python/model.py b/src-python/model.py index 377fcfe7..8f62aa5e 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -320,6 +320,7 @@ class Model: def checkSoftwareUpdated(): # check update update_flag = False + version = "" try: response = requests_get(config.GITHUB_URL) json_data = response.json() @@ -331,7 +332,10 @@ class Model: update_flag = True except Exception: errorLogging() - return update_flag + return { + "is_update_available": update_flag, + "new_version": version, + } @staticmethod def updateSoftware(): diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 347908d6..861e96ec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,7 +6,9 @@ version = 3 name = "VRCT" version = "0.0.0" dependencies = [ + "base64 0.22.1", "font-kit", + "reqwest", "serde", "serde_json", "tauri", @@ -218,6 +220,9 @@ name = "bytes" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +dependencies = [ + "serde", +] [[package]] name = "cairo-rs" @@ -729,7 +734,7 @@ dependencies = [ "rustc_version", "toml 0.8.19", "vswhom", - "winreg", + "winreg 0.52.0", ] [[package]] @@ -971,6 +976,12 @@ dependencies = [ "syn 2.0.94", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -984,8 +995,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1297,6 +1311,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1361,12 +1394,86 @@ dependencies = [ "itoa 1.0.14", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + [[package]] name = "http-range" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.14", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1613,6 +1720,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itoa" version = "0.4.8" @@ -1849,6 +1962,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -1859,6 +1978,34 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.6.0" @@ -1998,6 +2145,50 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2516,6 +2707,67 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg 0.50.0", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2544,6 +2796,37 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -2565,6 +2848,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -2577,6 +2869,39 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -2659,6 +2984,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.14", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.12.0" @@ -2784,6 +3121,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "soup2" version = "0.2.1" @@ -2812,6 +3159,12 @@ dependencies = [ "system-deps 5.0.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2881,6 +3234,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.13.1" @@ -2892,6 +3251,27 @@ dependencies = [ "syn 2.0.94", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -3000,6 +3380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22" dependencies = [ "anyhow", + "bytes", "cocoa 0.24.1", "dirs-next", "dunce", @@ -3014,6 +3395,7 @@ dependencies = [ "heck 0.5.0", "http", "ignore", + "indexmap 1.9.3", "log", "objc", "once_cell", @@ -3024,6 +3406,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "regex", + "reqwest", "semver", "serde", "serde_json", @@ -3296,7 +3679,44 @@ checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", + "libc", + "mio", "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -3367,6 +3787,12 @@ dependencies = [ "winnow 0.6.22", ] +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -3428,6 +3854,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -3446,6 +3878,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -3491,6 +3929,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.0.11" @@ -3539,6 +3983,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -3576,6 +4029,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.99" @@ -3605,6 +4071,29 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "0.18.2" @@ -3652,6 +4141,12 @@ dependencies = [ "system-deps 6.2.2", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webview2-com" version = "0.19.1" @@ -4058,6 +4553,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 06160d79..cf7e0ada 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -11,11 +11,13 @@ edition = "2021" tauri-build = { version = "1", features = [] } [dependencies] -tauri = { version = "1", features = [ "window-hide", "window-set-focus", "global-shortcut-all", "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] } +tauri = { version = "1", features = [ "fs-remove-dir", "fs-read-file", "fs-create-dir", "fs-write-file", "fs-exists", "http-request", "fs-read-dir", "window-hide", "window-set-focus", "global-shortcut-all", "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] } serde = { version = "1", features = ["derive"] } serde_json = "1" font-kit = "0.14.2" window-shadows = { git = "https://github.com/tauri-apps/window-shadows.git" } +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } +base64 = "0.22.1" [features] diff --git a/src-tauri/plugins/index.js b/src-tauri/plugins/index.js new file mode 100644 index 00000000..c8024261 --- /dev/null +++ b/src-tauri/plugins/index.js @@ -0,0 +1 @@ +// This is for preserving plugins folder. It will be detected and created 'plugins' folder to target/debug/ by tauri build. do not delete it. \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 163cc78a..36e07028 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -22,7 +22,7 @@ fn main() { event.window().set_size(tauri::Size::Physical(*new_inner_size)).unwrap(); } }) - .invoke_handler(tauri::generate_handler![get_font_list]) + .invoke_handler(tauri::generate_handler![get_font_list, download_zip_asset]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } @@ -45,4 +45,23 @@ async fn get_font_list() -> Vec { } font_families.into_iter().collect() +} + +#[tauri::command] +async fn download_zip_asset(url: String) -> Result { + use reqwest; + // reqwest のクライアントを作成 + let client = reqwest::Client::new(); + // GET リクエストを送信(リダイレクトも自動追従します) + let resp = client.get(&url) + .header("Accept", "application/octet-stream") + .send() + .await.map_err(|e| format!("Request error: {}", e))?; + if !resp.status().is_success() { + return Err(format!("HTTP error: {}", resp.status())); + } + // レスポンスのバイナリデータを取得 + let bytes = resp.bytes().await.map_err(|e| format!("Reading bytes error: {}", e))?; + // バイナリデータを base64 エンコードして返す + Ok(base64::encode(&bytes)) } \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 12896f1b..14b4c7b2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,6 +30,24 @@ "globalShortcut": { "all": true }, + "fs": { + "readDir": true, + "readFile": true, + "exists": true, + "writeFile": true, + "createDir": true, + "removeDir": true, + "scope": ["$RESOURCE/**", "**/src-tauri/target/debug/plugins/**"] + }, + "http": { + "request": true, + "scope": [ + "https://api.github.com/repos/**", + "https://github.com/**", + "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json", + "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/dev_vrct_plugins_list.json" + ] + }, "shell": { "all": false, "open": true, @@ -68,7 +86,8 @@ "bin/VRCT-sidecar" ], "resources": { - "bin/_internal": "_internal" + "bin/_internal": "_internal", + "plugins": "plugins" }, "windows": { "nsis": { diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index 86b5430d..c2b0e4f3 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -9,6 +9,7 @@ import { UiSizeController, FontFamilyController, TransparencyController, + PluginsController, } from "./_app_controllers/index.js"; import { WindowTitleBar } from "./window_title_bar/WindowTitleBar"; @@ -20,6 +21,7 @@ import { ModalController } from "./modal_controller/ModalController"; import { SnackbarController } from "./snackbar_controller/SnackbarController"; import styles from "./App.module.scss"; import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common"; +import { AppErrorBoundary } from "./error_boundary/AppErrorBoundary"; export const App = () => { const { currentIsVrctAvailable } = useIsVrctAvailable(); @@ -29,22 +31,24 @@ export const App = () => { return (
- - - - - - - - - + + + + + + + + + + - {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) - ? - : - } + {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) + ? + : + } - + +
); }; @@ -53,6 +57,8 @@ const Contents = () => { const { currentIsSoftwareUpdating } = useIsSoftwareUpdating(); return ( <> + + {currentIsSoftwareUpdating.data === false ? diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx new file mode 100644 index 00000000..9980f080 --- /dev/null +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import clsx from "clsx"; +import * as reactI18next from "react-i18next"; + +if (typeof window !== "undefined") { + window.React = React; + window.clsx = clsx; + window.reactI18next = reactI18next; +} + +import { LoadPluginsController } from "./plugins_controllers/LoadPluginsController"; +import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController"; +import { MergePluginsController } from "./plugins_controllers/MergePluginsController"; + +export const PluginsController = () => { + + return ( + <> + + + + + ); +}; \ No newline at end of file diff --git a/src-ui/app/_app_controllers/index.js b/src-ui/app/_app_controllers/index.js index 547c78f4..d71c8b6e 100644 --- a/src-ui/app/_app_controllers/index.js +++ b/src-ui/app/_app_controllers/index.js @@ -5,4 +5,5 @@ export { UiLanguageController } from "./UiLanguageController"; export { ConfigPageCloseTriggerController } from "./ConfigPageCloseTriggerController"; export { UiSizeController } from "./UiSizeController"; export { FontFamilyController } from "./FontFamilyController"; -export { TransparencyController } from "./TransparencyController"; \ No newline at end of file +export { TransparencyController } from "./TransparencyController"; +export { PluginsController } from "./PluginsController"; \ No newline at end of file diff --git a/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx b/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx new file mode 100644 index 00000000..1f9d5e75 --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import { usePlugins } from "@logics_configs"; + +export const FetchLatestPluginsDataController = () => { + const { + asyncFetchPluginsInfo, + isAnyPluginEnabled_Init, + } = usePlugins(); + + useEffect(() => { + if (isAnyPluginEnabled_Init()) { + asyncFetchPluginsInfo(); + } + }, []); + + return null; +}; \ No newline at end of file diff --git a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx new file mode 100644 index 00000000..46ad274b --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { usePlugins } from "@logics_configs"; +import { store } from "@store"; + +export const LoadPluginsController = () => { + const { + asyncLoadAllPlugins, + } = usePlugins(); + + const asyncInitLoadPlugins = async () => { + try { + await asyncLoadAllPlugins(); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + if (!store.is_initialized_load_plugin) { + asyncInitLoadPlugins(); + store.is_initialized_load_plugin = true; + } + }, []); + + return null; +}; \ No newline at end of file diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx new file mode 100644 index 00000000..255ede1c --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -0,0 +1,206 @@ +import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { store } from "@store"; +import { usePlugins } from "@logics_configs"; +import { useSoftwareVersion } from "@logics_common"; +import { useNotificationStatus } from "@logics_common"; + +export const MergePluginsController = () => { + const { t } = useTranslation(); + const { + currentLoadedPlugins, + updatePluginsData, + currentPluginsData, + currentFetchedPluginsInfo, + currentSavedPluginsStatus, + downloadAndExtractPlugin, + setTargetSavedPluginsStatus_Init, + } = usePlugins(); + const { checkVrctVerCompatibility } = useSoftwareVersion(); + const { showNotification_Success, showNotification_Error } = useNotificationStatus(); + + // downloaded, fetched, saved の各情報をまとめてマージ + useEffect(() => { + const mergePluginData = () => { + updatePluginsData(prev => { + // downloaded, fetched, 保存済み状態のMapをそれぞれ作成(plugin_id をキー) + const downloaded_map = new Map( + currentLoadedPlugins.data.map(info => [info.plugin_id, info]) + ); + const fetched_map = new Map( + currentFetchedPluginsInfo.data.map(info => [info.plugin_id, info]) + ); + const saved_map = new Map( + currentSavedPluginsStatus.data.map(saved => [saved.plugin_id, saved]) + ); + const prev_map = new Map( + prev.data.map(item => [item.plugin_id, item]) + ); + + // union_keys: Saved以外の情報に対して重複なくキーを取得する + const union_keys = new Set([ + ...downloaded_map.keys(), + ...fetched_map.keys(), + ...prev_map.keys(), + ]); + + const new_data = []; + for (const id of union_keys) { + const downloaded = downloaded_map.get(id); + const fetched = fetched_map.get(id); + const prev_plugin = prev_map.get(id); + let plugin = {}; + + if (downloaded) { + // ダウンロード済み情報に対してサポート確認 + const { is_plugin_supported, is_plugin_supported_latest_vrct } = + checkVrctVerCompatibility( + downloaded.min_supported_vrct_version, + downloaded.max_supported_vrct_version + ); + plugin = { + // prevの情報があれば引き継ぎつつ上書き + ...(prev_plugin || {}), + plugin_id: downloaded.plugin_id, + component: downloaded.component, + is_downloaded: true, + downloaded_plugin_info: { + ...downloaded, + is_plugin_supported, + is_plugin_supported_latest_vrct, + }, + }; + + if (fetched) { + const is_latest_version_available = + (downloaded.plugin_version !== fetched.plugin_version && fetched.is_plugin_supported); + plugin = { + ...plugin, + is_outdated: false, + latest_plugin_info: { ...fetched }, + is_latest_version_available: + plugin.is_downloaded && is_latest_version_available, + is_latest_version_already: + downloaded.plugin_version === fetched.plugin_version, + }; + } else { + // フェッチ情報がない場合の初期状態 + plugin = { + ...plugin, + is_latest_version_available: false, + is_latest_version_already: true, + }; + } + } else if (fetched) { + // フェッチ情報のみの場合は、ダウンロードしていない初期状態 + plugin = { + ...(prev_plugin || {}), + plugin_id: fetched.plugin_id, + is_downloaded: false, + is_latest_version_available: fetched.is_plugin_supported, + is_latest_version_already: false, + is_outdated: false, + latest_plugin_info: { ...fetched }, + }; + } else if (prev_plugin) { + // 既存情報のみ存在する場合は outdated フラグを付与 + plugin = { ...prev_plugin, is_outdated: true }; + } + // いずれかの情報がある場合のみ new_data に追加 + if (plugin.plugin_id) { + new_data.push(plugin); + } + } + + // 保存済み状態(currentSavedPluginsStatus)のマージ + // ・new_dataに存在する各プラグインに対して、保存済みの is_enabled を上書き + new_data.forEach(plugin => { + if (saved_map.has(plugin.plugin_id)) { + plugin.is_enabled = saved_map.get(plugin.plugin_id).is_enabled; + } + }); + // ・prev.data には存在せず、保存済み情報にのみある場合は追加 + for (const [id, saved] of saved_map.entries()) { + if (!new_data.some(item => item.plugin_id === id)) { + new_data.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); + } + } + + console.log("merged plugin data", new_data); + return new_data; + }); + }; + + mergePluginData(); + }, [currentFetchedPluginsInfo.data, currentLoadedPlugins.data, currentSavedPluginsStatus]); + + + + // --- 自動アップデート(ダウンロード処理)--- + // ※downloadAndExtractPlugin の重複実行を防ぐため、実行中の plugin_id を useRef で管理 + const downloadingRef = useRef(new Set()); + + useEffect(() => { + if (!currentPluginsData.data.length) return; + // マージ結果の currentPluginsData.data を元にダウンロード処理をチェック + currentPluginsData.data.forEach(plugin => { + if (plugin.is_downloaded && + plugin.is_enabled && + !plugin.downloaded_plugin_info.is_plugin_supported && + !plugin.is_latest_version_already && + plugin.is_latest_version_available + ) { + if (!downloadingRef.current.has(plugin.plugin_id)) { + showNotification_Success(t("plugin_notifications.updating")); + downloadingRef.current.add(plugin.plugin_id); + const target_plugin_id = plugin.plugin_id; + downloadAndExtractPlugin(plugin) + .then(() => { + console.log(`Plugin ${target_plugin_id} updated successfully`); + downloadingRef.current.delete(target_plugin_id); + showNotification_Success(t("plugin_notifications.updated_success")); + }) + .catch((error) => { + console.error(`Plugin ${target_plugin_id} update failed`, error); + downloadingRef.current.delete(target_plugin_id); + showNotification_Error(t("plugin_notifications.updated_error")); + }); + } + } + }); + }, [currentPluginsData.data]); + + + useEffect(() => { + // ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化 + if (store.is_initialized_fetched_plugin_info) { + updatePluginsData(prev => { + prev.data.forEach(plugin => { + if (plugin.is_downloaded && plugin.is_enabled) { + if ( + !plugin.downloaded_plugin_info.is_plugin_supported && + plugin.latest_plugin_info && + !plugin.latest_plugin_info?.is_plugin_supported + ) { + showNotification_Error(t("plugin_notifications.disabled_out_of_support")); + plugin.is_enabled = false; + setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); + } + + if ( + !plugin.downloaded_plugin_info.is_plugin_supported && + plugin.is_outdated + ) { + showNotification_Error(t("plugin_notifications.disabled_out_of_support")); + plugin.is_enabled = false; + setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); + } + } + }); + return prev.data; + }); + } + }, [store.is_initialized_fetched_plugin_info]); + + return null; +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx b/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx index e61067a7..9fc67c13 100644 --- a/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx @@ -9,6 +9,7 @@ import { AdvancedSettings, Vr, Hotkeys, + // Plugins, Supporters, AboutVrct, } from "@setting_box"; @@ -32,6 +33,8 @@ export const SettingBox = () => { return ; case "advanced_settings": return ; + // case "plugins": + // return ; case "supporters": return ; case "about_vrct": diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.jsx new file mode 100644 index 00000000..edaf7fa5 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.jsx @@ -0,0 +1,50 @@ +import { useTranslation } from "react-i18next"; +import CircularProgress from "@mui/material/CircularProgress"; +import styles from "./_DownloadButton.module.scss"; + +export const _DownloadButton = ({option, ...props}) => { + const { t } = useTranslation(); + + const renderContent = () => { + const circular_progress = Math.floor(option.progress / 10) * 10; + + switch (true) { + case option.progress !== null: + return ( + <> + +

{`${Math.round(option.progress)}%`}

+ + ); + case option.is_pending: + return ; + case !option.is_downloaded: + return ( + + ); + case option.update_button: + return ( + + ); + default: + return null; + } + }; + + return
{renderContent()}
; +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.module.scss new file mode 100644 index 00000000..752dca38 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.module.scss @@ -0,0 +1,45 @@ +@import "@scss_mixins"; + +.download_container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 8rem; +} + +.download_button { + pointer-events: auto; + background-color: var(--dark_800_color); + padding: 0.8rem; + flex-shrink: 0; + border-radius: 0.2rem; + &:hover { + background-color: var(--dark_750_color); + } + &:active { + background-color: var(--dark_800_color); + } +} +.download_button_label { + font-size: 1.2rem; +} + +.progress_label { + position: absolute; + font-size: 1rem; +} + +.update_button { + pointer-events: auto; + background-color: var(--primary_400_color); + padding: 0.8rem; + flex-shrink: 0; + border-radius: 0.2rem; + &:hover { + background-color: var(--primary_450_color); + } + &:active { + background-color: var(--primary_500_color); + } +} \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.jsx index b1f2360b..73656a0f 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.jsx @@ -1,9 +1,7 @@ -import { useTranslation } from "react-i18next"; -import CircularProgress from "@mui/material/CircularProgress"; -import styles from "./DownloadModels.module.scss"; import { RadioButton, } from "../index"; +import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; export const DownloadModels = (props) => { const options = props.options.map(item => ({ @@ -19,47 +17,9 @@ export const DownloadModels = (props) => { options={options} checked_variable={props.checked_variable} column={true} - ChildComponent={ModelSelector} + ChildComponent={_DownloadButton} downloadStartFunction={props.downloadStartFunction} /> ); -}; - -const ModelSelector = ({option, ...props}) => { - const { t } = useTranslation(); - - const renderContent = () => { - const circular_progress = Math.floor(option.progress / 10) * 10; - - switch (true) { - case option.progress !== null: - return ( - <> - -

{`${Math.round(option.progress)}%`}

- - ); - case option.is_pending: - return ; - case !option.is_downloaded: - return ( - - ); - default: - return null; - } - }; - - return
{renderContent()}
; -}; +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.module.scss index 59fabfe3..e69de29b 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/_components/download_models/DownloadModels.module.scss @@ -1,30 +0,0 @@ -@import "@scss_mixins"; - -.download_container { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - max-width: 8rem; -} - -.download_button { - pointer-events: auto; - background-color: var(--dark_800_color); - padding: 0.8rem; - flex-shrink: 0; - &:hover { - background-color: var(--dark_750_color); - } - &:active { - background-color: var(--dark_800_color); - } -} -.download_button_label { - font-size: 1.2rem; -} - -.progress_label { - position: absolute; - font-size: 1rem; -} \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx index 750095e0..4e9efb65 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/hotkeys_entry/HotkeysEntry.jsx @@ -2,7 +2,7 @@ import styles from "./HotkeysEntry.module.scss"; import { _Entry } from "../_atoms/_entry/_Entry"; import { useState, useRef, useEffect } from "react"; import DeleteSvg from "@images/cancel.svg?react"; -import { clsx } from "clsx"; +import clsx from "clsx"; export const HotkeysEntry = (props) => { const [isAcceptingInput, setIsAcceptingInput] = useState(false); diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/index.js b/src-ui/app/config_page/setting_section/setting_box/_components/index.js index 7b695b35..178dbcec 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/index.js +++ b/src-ui/app/config_page/setting_section/setting_box/_components/index.js @@ -12,4 +12,5 @@ export { Slider } from "./slider/Slider"; export { SwitchBox } from "./switch_box/SwitchBox"; export { ThresholdComponent } from "./threshold_component/ThresholdComponent"; export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter"; -export { DownloadModels } from "./download_models/DownloadModels"; \ No newline at end of file +export { DownloadModels } from "./download_models/DownloadModels"; +export { PluginsControlComponent } from "./plugins_control_component/PluginsControlComponent"; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.jsx new file mode 100644 index 00000000..c7c6ace0 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.jsx @@ -0,0 +1,156 @@ +import React from "react"; +import { SwitchBox } from "../index"; +import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; +import styles from "./PluginsControlComponent.module.scss"; +import { useTranslation } from "react-i18next"; + +export const PluginsControlComponent = ({ + variable_state, + plugin_status, + toggleFunction, + downloadStartFunction, +}) => { + const { t } = useTranslation(); + + const option = { + id: plugin_status.plugin_id, + is_pending: plugin_status.is_pending, + is_downloaded: plugin_status.is_downloaded, + data: plugin_status.is_enabled, + update_button: plugin_status.is_downloaded && plugin_status.is_latest_version_available, + state: variable_state, + progress: null, + }; + + const downloaded_version = plugin_status.downloaded_plugin_info?.plugin_version; + const latest_version = plugin_status.latest_plugin_info?.plugin_version; + + const downloaded_version_label = t("config_page.plugins.downloaded_version", + { downloaded_version: downloaded_version } + ); + const latest_version_label = t("config_page.plugins.latest_version", + { latest_version: latest_version } + ); + + if (plugin_status.is_downloaded) { + return ( + + ); + } else { + return ( + + ); + } +}; + + +const DownloadedPluginControl = ({ + option, + plugin_status, + toggleFunction, + downloadStartFunction, + downloaded_version_label, + latest_version_label, +}) => { + const { t } = useTranslation(); + + const togglePlugin = () => { + toggleFunction(plugin_status.plugin_id); + }; + + if (!plugin_status.downloaded_plugin_info.is_plugin_supported) { + if (plugin_status.is_latest_version_available) { + return ( +
+

{downloaded_version_label}

+

{latest_version_label}

+

{t("config_page.plugins.available_after_updating")}

+ <_DownloadButton option={option} downloadStartFunction={downloadStartFunction} /> +
+ ); + } + return ( +
+

{t("config_page.plugins.unavailable_downloaded")}

+
+ ); + } else if (plugin_status.is_outdated) { + return ( +
+

{t("config_page.plugins.no_latest_info")}

+ +
+ ); + } else if (plugin_status.is_latest_version_already) { + return ( +
+

{latest_version_label}

+

{t("config_page.plugins.using_latest_version")}

+ +
+ ); + } else if (plugin_status.is_latest_version_available) { + return ( +
+

{latest_version_label}

+

{t("config_page.plugins.available_latest_version")}

+ <_DownloadButton option={option} downloadStartFunction={downloadStartFunction} /> + +
+ ); + } else { + return ( +
+

{t("config_page.plugins.available_latest_version")}

+ +
+ ); + } +}; + + +const NotDownloadedPluginControl = ({ + option, + plugin_status, + downloadStartFunction, + downloaded_version_label, + latest_version_label, +}) => { + const { t } = useTranslation(); + + if (plugin_status.is_latest_version_available) { + return ( +
+

{latest_version_label}

+ <_DownloadButton option={option} downloadStartFunction={downloadStartFunction} /> +
+ ); + } else if (plugin_status.latest_plugin_info?.is_plugin_supported_latest_vrct) { + return ( +
+

{latest_version_label}

+

{t("config_page.plugins.available_in_latest_vrct_version")}

+
+ ); + } else { + return ( +
+

{latest_version_label}

+

{t("config_page.plugins.unavailable_not_downloaded")}

+
+ ); + } +}; diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.module.scss new file mode 100644 index 00000000..816119fc --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.module.scss @@ -0,0 +1,11 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; +} + +.unavailable_text { + padding: 1rem; + font-size: 1.2rem; +} \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx index 07c70ff2..8f61d725 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx @@ -1,7 +1,7 @@ import React from "react"; import styles from "./Slider.module.scss"; import MUI_Slider from "@mui/material/Slider"; -import { clsx } from "clsx"; +import clsx from "clsx"; export const Slider = (props) => { return ( diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/switch_box/SwitchBox.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/switch_box/SwitchBox.module.scss index 8f76c0b6..87bf6600 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/switch_box/SwitchBox.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/_components/switch_box/SwitchBox.module.scss @@ -1,7 +1,6 @@ @import "@scss_mixins"; .switchbox_container { - width: 100%; display: flex; justify-content: end; align-items: center; @@ -22,6 +21,7 @@ } .toggle_control { + position: relative; @include toggle_control_styles; display: flex; justify-content: center; diff --git a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx index b96c7fa9..206ed7ca 100644 --- a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx @@ -2,6 +2,8 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import styles from "./AdvancedSettings.module.scss"; +import { Plugins } from "./plugins/Plugins"; + import { useOpenFolder } from "@logics_common"; import { useOscIpAddress, @@ -10,22 +12,35 @@ import { import { ActionButtonContainer, - EntryContainer, EntryWithSaveButtonContainer, } from "../_templates/Templates"; +import { + SectionLabelComponent, +} from "../_components/"; + import OpenFolderSvg from "@images/open_folder.svg?react"; import HelpSvg from "@images/help.svg?react"; export const AdvancedSettings = () => { + const { t } = useTranslation(); + return ( - <> - - - - - +
+
+ + + + + +
+ +
+ + +
+
); }; diff --git a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.module.scss b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.module.scss index fa5eefb3..a49fed11 100644 --- a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.module.scss @@ -1,22 +1,5 @@ .container { display: flex; - width: 100%; - justify-content: space-between; - align-items: center; - padding: 2rem; - align-items: center; - gap: 2rem; - &.flex_column { - flex-direction: column; - } - border-bottom: solid 0.1rem var(--dark_800_color); -} - -.switch_section_container { - display: flex; - width: 100%; - justify-content: space-between; - align-items: center; - align-items: center; - gap: 2rem; + gap: 6.4rem; + flex-direction: column; } \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.jsx new file mode 100644 index 00000000..b19d48d7 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.jsx @@ -0,0 +1,119 @@ +import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { usePlugins } from "@logics_configs"; +import styles from "./Plugins.module.scss"; +import { PluginsControlComponent } from "../../_components/plugins_control_component/PluginsControlComponent"; +import { useNotificationStatus } from "@logics_common"; + +export const Plugins = () => { + const { + asyncFetchPluginsInfo, + } = usePlugins(); + const hasRunRef = useRef(false); + + useEffect(() => { + if (!hasRunRef.current) { + asyncFetchPluginsInfo(); + } + return () => hasRunRef.current = true; + }, []); + + return ( +
+ +
+ ); +}; + +const PluginDownloadContainer = () => { + const { t, i18n } = useTranslation(); + const { + downloadAndExtractPlugin, + currentPluginsData, + currentSavedPluginsStatus, + toggleSavedPluginsStatus, + handlePendingPlugin, + currentFetchedPluginsInfo, + } = usePlugins(); + const { showNotification_Success, showNotification_Error } = useNotificationStatus(); + + // ダウンロード開始時の状態更新処理 + const downloadStartFunction = async (target_plugin_id) => { + handlePendingPlugin(target_plugin_id, true); + showNotification_Success(t("plugin_notifications.downloading")); + + const target_plugin_info = currentPluginsData.data.find( + (d) => d.plugin_id === target_plugin_id + ); + downloadAndExtractPlugin(target_plugin_info).then(() => { + handlePendingPlugin(target_plugin_id, false); + showNotification_Success(t("plugin_notifications.downloaded_success")); + }).catch(error => { + console.error(error); + showNotification_Error(t("plugin_notifications.downloaded_error")); + }); + }; + + // プラグインのオンオフ切り替え処理 + const toggleFunction = (target_plugin_id) => { + toggleSavedPluginsStatus(target_plugin_id); + }; + + const variable_state = currentSavedPluginsStatus.state; + + const filtered_plugins_data = currentPluginsData.data.filter(plugin => !plugin.is_outdated) + + // plugin_id で ABC 順にソート + const sorted_plugins_data = filtered_plugins_data.sort((a, b) => + a.plugin_id.localeCompare(b.plugin_id) + ); + + // Duplicate + const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error"; + const is_fetching = currentFetchedPluginsInfo.state === "pending"; + + return ( +
+ {is_failed_to_fetch &&

Failed to fetch plugins data

} + {is_fetching &&

Fetching plugins data...

} + {sorted_plugins_data.map((plugin) => { + const target_info = plugin.is_downloaded + ? plugin.downloaded_plugin_info + : plugin.latest_plugin_info; + + const target_locale = target_info.locales && target_info.locales[i18n.language] + ? target_info.locales[i18n.language] + : { + title: target_info.title, + desc: target_info.desc || null, + }; + + return ( +
+
+

+ {target_locale.title} +

+

+ {target_locale.desc} +

+ {/*

{plugin.plugin_id}

*/} +
+
+ {plugin.is_error ? ( +

Error: {plugin.error_message}

+ ) : ( + + )} +
+
+ ); + })} +
+ ); +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.module.scss new file mode 100644 index 00000000..af4bae9a --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.module.scss @@ -0,0 +1,58 @@ +.container { + display: flex; + gap: 6.4rem; + flex-direction: column; +} + +.plugins_list_container { + display: flex; + flex-direction: column; + align-items: center; +} + +.plugin_wrapper { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem; + gap: 2rem; + &:not(:last-child) { + border-bottom: 0.1rem solid var(--dark_750_color); + } +} + +.labels_wrapper { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-width: 50%; +} + +.plugin_info_wrapper { + display: flex; + justify-content: end; + align-items: center; +} + +.title { + font-size: 1.6rem; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.desc { + font-size: 1.4rem; + width: 100%; + overflow: hidden; + color: var(--dark_500_color); +} +// .plugin_id { +// font-size: 1rem; +// color: var(--dark_600_color); +// width: 100%; +// overflow: hidden; +// white-space: nowrap; +// text-overflow: ellipsis; +// } \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/index.js b/src-ui/app/config_page/setting_section/setting_box/index.js index dc5798c3..80cfca10 100644 --- a/src-ui/app/config_page/setting_section/setting_box/index.js +++ b/src-ui/app/config_page/setting_section/setting_box/index.js @@ -6,5 +6,6 @@ export { Others, VrcMicMuteSyncContainer } from "./others/Others"; export { AdvancedSettings } from "./advanced_settings/AdvancedSettings"; export { Vr } from "./vr/Vr"; export { Hotkeys } from "./hotkeys/Hotkeys"; +// export { Plugins } from "./plugins/Plugins"; export { AboutVrct } from "./about_vrct/AboutVrct"; export { Supporters } from "./supporters/Supporters"; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/supporters/support_us_container/SupportUsContainer.jsx b/src-ui/app/config_page/setting_section/setting_box/supporters/support_us_container/SupportUsContainer.jsx index 7c357a9f..1c305fe6 100644 --- a/src-ui/app/config_page/setting_section/setting_box/supporters/support_us_container/SupportUsContainer.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/supporters/support_us_container/SupportUsContainer.jsx @@ -3,7 +3,7 @@ import fanbox_logo from "@images/supporters/fanbox_logo.png"; import kofi_logo from "@images/supporters/kofi_logo.png"; import patreon_logo from "@images/supporters/patreon_logo.png"; import styles from "./SupportUsContainer.module.scss"; -import { clsx } from "clsx"; +import clsx from "clsx"; export const SupportUsContainer = () => { return ( diff --git a/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx b/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx index e1197b22..06920712 100644 --- a/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { clsx } from "clsx"; +import clsx from "clsx"; import styles from "./Vr.module.scss"; import { ui_configs } from "@ui_configs"; import { Slider } from "../_components/"; diff --git a/src-ui/app/config_page/sidebar_section/SidebarSection.jsx b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx index 51eddec8..f845a733 100644 --- a/src-ui/app/config_page/sidebar_section/SidebarSection.jsx +++ b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx @@ -12,6 +12,7 @@ export const SidebarSection = () => { + {/* */}
diff --git a/src-ui/app/config_page/version_label/VersionLabel.jsx b/src-ui/app/config_page/version_label/VersionLabel.jsx index f6325165..95abd94d 100644 --- a/src-ui/app/config_page/version_label/VersionLabel.jsx +++ b/src-ui/app/config_page/version_label/VersionLabel.jsx @@ -1,10 +1,9 @@ import { useTranslation } from "react-i18next"; import { useState } from "react"; -import { clsx } from "clsx"; +import clsx from "clsx"; import styles from "./VersionLabel.module.scss"; -import { useSoftwareVersion } from "@logics_configs"; -import { useComputeMode } from "@logics_common"; +import { useSoftwareVersion, useComputeMode } from "@logics_common"; import CopySvg from "@images/copy.svg?react"; import CheckMarkSvg from "@images/check_mark.svg?react"; diff --git a/src-ui/app/error_boundary/AppErrorBoundary.jsx b/src-ui/app/error_boundary/AppErrorBoundary.jsx new file mode 100644 index 00000000..cb8550a0 --- /dev/null +++ b/src-ui/app/error_boundary/AppErrorBoundary.jsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { appWindow } from "@tauri-apps/api/window"; +import { ErrorBoundary } from "react-error-boundary"; +import XMarkSvg from "@images/cancel.svg?react"; +import CopySvg from "@images/copy.svg?react"; +import CheckMarkSvg from "@images/check_mark.svg?react"; + +import { ContactsContainer } from "./contacts_container/ContactsContainer"; + +import styles from "./AppErrorBoundary.module.scss"; + +export const AppErrorBoundary = ({children}) => { + return ( + ( + + ) + }> + {children} + + ); +}; + +const ErrorContainer = ({error}) => { + const [is_copied, setIsCopied] = useState(false); + + const formatted_stack = error ? formatStackTrace(error.stack) : "Unknown error"; + + const copyToClipboard = async () => { + if (is_copied) return; + + await navigator.clipboard.writeText(formatted_stack); + setIsCopied(true); + + setTimeout(() => { + setIsCopied(false); + }, 1000); + }; + + return ( +
+ +
+

An error occurred. Please restart VRCT or contact the developers.

+ {error ? +
+
+

+ {formatted_stack} +

+
+ +
+ : null} + +
+
+ ); +}; + +const CloseButtonContainer = () => { + const close = () => { + appWindow.close(); + }; + + return ( + + ); +}; + + +const formatStackTrace = (stack) => { + if (!stack) return ""; + // フルパスの除去(例として window.location.origin や絶対パス部分を削除) + // ※必要に応じて正規表現を調整してください + const formatted = stack.replace(new RegExp(window.location.origin, "g"), ""); + + return formatted; +}; \ No newline at end of file diff --git a/src-ui/app/error_boundary/AppErrorBoundary.module.scss b/src-ui/app/error_boundary/AppErrorBoundary.module.scss new file mode 100644 index 00000000..2abb2710 --- /dev/null +++ b/src-ui/app/error_boundary/AppErrorBoundary.module.scss @@ -0,0 +1,110 @@ +.container { + width: 100%; + height: 100%; + position: relative; +} + +.wrapper { + width: 100%; + height: 100vh; + display: flex; + justify-content: safe center; + align-items: center; + flex-direction: column; + padding: 2rem; + overflow-y: auto; +} + +.error_message { + font-size: 2rem; + text-align: center; + user-select: text; + margin-bottom: 3.2rem; +} + + +.error_detail_container { + display: flex; + flex-direction: column; + align-items: end; + gap: 1rem; +} +.error_stack_container { + max-height: 10rem; + width: 100%; + overflow-y: scroll; + padding: 1rem; + background-color: var(--dark_950_color); + border-radius: 0.4rem; +} +.error_stack { + font-size: 1rem; + user-select: text; +} + +.copy_error_message_button { + // background-color: var(--dark_800_color); + padding: 0.8rem 1rem; + font-size: 1.4rem; + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + border-radius: 0.4rem; + background-color: var(--dark_825_color); + &:hover { + background-color: var(--dark_800_color); + } + &:active { + background-color: var(--dark_850_color); + } +} + +.copy_svg { + width: 1.4rem; + color: var(--dark_500_color); +} + +.check_mark_svg { + width: 1.4rem; + color: var(--primary_300_color); +} + + + +.close_button_wrapper { + position: absolute; + top: 0; + left: 100%; + transform: translate(-50%, -50%) rotate(45deg); + display: flex; + justify-content: center; + align-items: end; + width: 68px; + aspect-ratio: 1 / 1; + background-color: var(--error_bc_color); + & .x_mark_svg { + color: var(--dark_200_color); + } + &:hover { + & .x_mark_svg { + transform: rotate(45deg); + } + } + &:active { + background-color: var(--error_bc_active_color); + } + transition: all 0.1s ease; +} + +.close_button { + // width: 100%; + // height: 100%; +} + +.x_mark_svg { + width: 24px; + transform: rotate(-45deg); + color: var(--dark_700_color); + transition: transform 0.3s ease; +} \ No newline at end of file diff --git a/src-ui/app/error_boundary/contacts_container/ContactsContainer.jsx b/src-ui/app/error_boundary/contacts_container/ContactsContainer.jsx new file mode 100644 index 00000000..965e7365 --- /dev/null +++ b/src-ui/app/error_boundary/contacts_container/ContactsContainer.jsx @@ -0,0 +1,29 @@ +import styles from "./ContactsContainer.module.scss"; + +export const ContactsContainer = () => { + return ( +
+ + +
+ ); +}; + +import dev_github_icon from "@images/about_vrct/dev_github_icon.png"; +import document from "@images/document.png"; + +const contacts_links = { + github_issues: { img: dev_github_icon, href: "https://github.com/misyaguziya/VRCT/issues" }, + google_forms: { img: document, href: "https://docs.google.com/forms/d/e/1FAIpQLSei-xoydOY60ivXqhOjaTzNN8PiBQIDcNhzfy6cw2sjYkcg_g/viewform" }, +}; + +const OpenLinkContainer = ({className, href_id, text}) => { + const href = contacts_links[href_id].href; + const img = contacts_links[href_id].img; + return ( + + +

{text}

+
+ ); +}; \ No newline at end of file diff --git a/src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss b/src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss new file mode 100644 index 00000000..5e291d0f --- /dev/null +++ b/src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss @@ -0,0 +1,28 @@ +.container { + display: flex; + gap: 3.2rem; +} + +.github_issues, .google_forms { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + padding: 1rem; + border-radius: 0.4rem; + gap: 1rem; + &:hover { + background-color: var(--dark_800_color); + } + &:active { + background-color: var(--dark_900_color) + } +} +.contact_button_icon { + width: 5.2rem; +} +.contact_button_label { + font-size: 1.4rem; + white-space: nowrap; +} \ No newline at end of file diff --git a/src-ui/app/main_page/main_section/MainSection.jsx b/src-ui/app/main_page/main_section/MainSection.jsx index faf910bd..b0ffb77c 100644 --- a/src-ui/app/main_page/main_section/MainSection.jsx +++ b/src-ui/app/main_page/main_section/MainSection.jsx @@ -9,12 +9,27 @@ import { useStore_IsOpenedLanguageSelector } from "@store"; import { useLanguageSettings } from "@logics_main"; import { useEffect } from "react"; +import { PluginHost } from "./PluginHost"; + +import { usePlugins } from "@logics_configs"; + export const MainSection = () => { + const { currentPluginsData } = usePlugins(); + + const render_plugins = currentPluginsData.data.filter((plugin) => ( + plugin.is_downloaded && + plugin.is_enabled && + plugin.downloaded_plugin_info.is_plugin_supported && + plugin.downloaded_plugin_info.location === "main_section" + )); return (
- + {render_plugins.length + ? + : + }
); diff --git a/src-ui/app/main_page/main_section/MainSection.module.scss b/src-ui/app/main_page/main_section/MainSection.module.scss index 29a7907a..c132e869 100644 --- a/src-ui/app/main_page/main_section/MainSection.module.scss +++ b/src-ui/app/main_page/main_section/MainSection.module.scss @@ -4,7 +4,8 @@ height: 100%; display: flex; flex-direction: column; - justify-content: space-between; + // justify-content: space-between; + overflow-y: auto; } .language_selector_container { diff --git a/src-ui/app/main_page/main_section/PluginHost.jsx b/src-ui/app/main_page/main_section/PluginHost.jsx new file mode 100644 index 00000000..071ac934 --- /dev/null +++ b/src-ui/app/main_page/main_section/PluginHost.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +export const PluginHost = ({render_components}) => { + + return ( + <> + {render_components + .map((plugin, index) => { + const PluginComponent = plugin.component; + return PluginComponent ? : null; + })} + + ); +}; \ No newline at end of file diff --git a/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx b/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx index 995266e2..1917d523 100644 --- a/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx +++ b/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx @@ -4,7 +4,7 @@ import RefreshSvg from "@images/refresh.svg?react"; import HelpSvg from "@images/help.svg?react"; import { useStore_OpenedQuickSetting } from "@store"; -import { useIsSoftwareUpdateAvailable } from "@logics_common"; +import { useSoftwareVersion } from "@logics_common"; import { useIsEnabledOverlaySmallLog, useIsEnabledOverlayLargeLog, useEnableVrcMicMuteSync } from "@logics_configs"; import { OpenQuickSettingButton } from "./_buttons/OpenQuickSettingButton"; @@ -66,9 +66,9 @@ const OpenVrcMicMuteSyncQuickSetting = () => { }; const SoftwareUpdateAvailableButton = () => { - const { currentIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable(); + const { currentLatestSoftwareVersionInfo } = useSoftwareVersion(); const { t } = useTranslation(); - if (currentIsSoftwareUpdateAvailable.data === false) return null; + if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null; const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting(); diff --git a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx index 94335cd2..c35a240c 100644 --- a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx +++ b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx @@ -1,9 +1,16 @@ +import clsx from "clsx"; import styles from "./UpdateModal.module.scss"; import { useTranslation } from "react-i18next"; import { useStore_OpenedQuickSetting } from "@store"; -import { useComputeMode, useUpdateSoftware } from "@logics_common"; -import { useIsSoftwareUpdating, useIsSoftwareUpdateAvailable } from "@logics_common"; -import clsx from "clsx"; +import { usePlugins } from "@logics_configs"; +import { + useComputeMode, + useUpdateSoftware, + useIsSoftwareUpdating, + useSoftwareVersion, +} from "@logics_common"; + +import { PluginCompatibilityList } from "./plugins_compatibility_list/PluginCompatibilityList"; export const UpdateModal = () => { const { t } = useTranslation(); @@ -11,9 +18,10 @@ export const UpdateModal = () => { const { updateSoftware, updateSoftware_CUDA } = useUpdateSoftware(); const { updateIsSoftwareUpdating } = useIsSoftwareUpdating(); const { currentComputeMode } = useComputeMode(); - const { currentIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable(); + const { currentLatestSoftwareVersionInfo } = useSoftwareVersion(); + const { isAnyPluginEnabled } = usePlugins(); - const is_latest_version_already = currentIsSoftwareUpdateAvailable.data === false; + const is_latest_version_already = currentLatestSoftwareVersionInfo.data.is_update_available === false; const is_cpu_version = currentComputeMode.data === "cpu"; const onClickUpdateSoftware = () => { @@ -37,30 +45,34 @@ export const UpdateModal = () => { return (
-
-
-
- - {is_cpu_version ? : null} +
+ {isAnyPluginEnabled() && } +
+
+
+ + {is_cpu_version ? : null} +
+
+ +
-
- +
+
+ + {!is_cpu_version ? : null} +
+
+ + + +
-
-
-
- - {!is_cpu_version ? : null} -
-
- - - -
-
-

{t("update_modal.download_latest_and_restart")}

+

{t("update_modal.download_latest_and_restart")}

+
+
diff --git a/src-ui/app/modal_controller/update_modal/UpdateModal.module.scss b/src-ui/app/modal_controller/update_modal/UpdateModal.module.scss index 57881896..4fe27802 100644 --- a/src-ui/app/modal_controller/update_modal/UpdateModal.module.scss +++ b/src-ui/app/modal_controller/update_modal/UpdateModal.module.scss @@ -3,7 +3,7 @@ height: 100%; display: flex; flex-direction: column; - justify-content: center; + justify-content: safe center; align-items: center; gap: 2.4rem; } @@ -16,6 +16,14 @@ gap: 8rem; } +.update_section_wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4rem; +} + .update_section { border: 0.1rem solid var(--dark_600_color); border-radius: 0.4rem; diff --git a/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.jsx b/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.jsx new file mode 100644 index 00000000..e23c320c --- /dev/null +++ b/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.jsx @@ -0,0 +1,78 @@ +import { useEffect } from "react"; +import styles from "./PluginCompatibilityList.module.scss"; +import { usePlugins } from "@logics_configs"; +import CheckMarkSvg from "@images/check_mark.svg?react"; +import XSvg from "@images/x_mark.svg?react"; +import WarningSvg from "@images/warning.svg?react"; + +export const PluginCompatibilityList = () => { + const { + enabledPluginsList, + asyncFetchPluginsInfo, + currentFetchedPluginsInfo, + } = usePlugins(); + + useEffect(() => { + asyncFetchPluginsInfo(); + }, []); + + // ダウンロード済みのもの + const downloaded_plugin = enabledPluginsList().filter(p => p.is_downloaded); + + const compatible_plugins_list = []; + const incompatible_plugins_list = []; + for (const p of downloaded_plugin) { + if (!p.downloaded_plugin_info?.is_plugin_supported_latest_vrct && !p.latest_plugin_info?.is_plugin_supported_latest_vrct) { + // プラグイン最新版でも、VRCT最新版(VRCTアプデ後)に非対応のもの + incompatible_plugins_list.push(p); + } else { + // 現プラグイン or 最新版が、VRCT最新版(VRCTアプデ後)に対応しているもの + compatible_plugins_list.push(p); + } + } + + const is_any_incompatible_plugin = incompatible_plugins_list.length > 0; + const is_any_compatible_plugin = compatible_plugins_list.length > 0; + + if (!is_any_incompatible_plugin && !is_any_compatible_plugin) return null; // This is just for safety. + + // Duplicate + const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error"; + const is_fetching = currentFetchedPluginsInfo.state === "pending"; + + return ( +
+

使用中プラグインの互換性チェック

+ {is_failed_to_fetch &&

Failed to fetch plugins data

} + {is_fetching &&

Fetching plugins data...

} +
+ {incompatible_plugins_list.map(plugin => { + const target_data = plugin.downloaded_plugin_info; + return ; + })} + {compatible_plugins_list.map(plugin => { + const target_data = plugin.downloaded_plugin_info; + return ; + })} +
+ {is_any_incompatible_plugin && +
+ +

VRCT最新バージョンで互換性のないプラグインはアップデート後に無効化されます。引き続き使用したい場合は、各プラグインの更新を待ってください。

+
+ } +
+ ); +}; + +const PluginContainer = ({ target_data, is_compatible }) => { + return ( +
+

{target_data.title}

+ {is_compatible + ? + : + } +
+ ); +}; \ No newline at end of file diff --git a/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.module.scss b/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.module.scss new file mode 100644 index 00000000..0b439619 --- /dev/null +++ b/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.module.scss @@ -0,0 +1,63 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 1rem; +} + +.title { + font-size: 1.6rem; +} + +.plugins_compatibility_container { + display: flex; + justify-content: center; + align-items: center; + gap: 0.2rem 1rem; + flex-wrap: wrap; +} + +.plugin_box { + display: flex; + justify-content: center; + align-items: center; + padding: 0.4rem 0.6rem; + gap: 0.6rem; +} + +.plugin_label { + font-size: 1.4rem; + color: var(--error_bc_color); + &.is_compatible { + color: var(--primary_300_color); + } +} + + +.check_mark_svg { + width: 1.8rem; + color: var(--primary_300_color); +} +.x_svg { + width: 1.8rem; + color: var(--error_bc_color); +} + +.warning_container { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; +} + +.warning_svg { + padding-bottom: 0.4rem; + width: 2.4rem; + color: var(--waring_color); + flex-shrink: 0; +} + +.warning_text { + font-size: 1.2rem; +} \ No newline at end of file diff --git a/src-ui/app/snackbar_controller/SnackbarController.jsx b/src-ui/app/snackbar_controller/SnackbarController.jsx index 0c116c09..0b7c9609 100644 --- a/src-ui/app/snackbar_controller/SnackbarController.jsx +++ b/src-ui/app/snackbar_controller/SnackbarController.jsx @@ -1,4 +1,4 @@ -import { clsx } from "clsx"; +import clsx from "clsx"; import Snackbar from "@mui/material/Snackbar"; import Slide from "@mui/material/Slide"; diff --git a/src-ui/assets/document.png b/src-ui/assets/document.png new file mode 100644 index 00000000..cfe54e2d Binary files /dev/null and b/src-ui/assets/document.png differ diff --git a/src-ui/assets/x_mark.svg b/src-ui/assets/x_mark.svg new file mode 100644 index 00000000..e4a0922e --- /dev/null +++ b/src-ui/assets/x_mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-ui/common_components/checkbox/Checkbox.jsx b/src-ui/common_components/checkbox/Checkbox.jsx index b9cdd7d3..50ee51a9 100644 --- a/src-ui/common_components/checkbox/Checkbox.jsx +++ b/src-ui/common_components/checkbox/Checkbox.jsx @@ -1,4 +1,4 @@ -import { clsx } from "clsx"; +import clsx from "clsx"; import styles from "./Checkbox.module.scss"; export const Checkbox = ({ checkboxId, diff --git a/src-ui/logics/common/index.js b/src-ui/logics/common/index.js index 035f844b..e95bae8f 100644 --- a/src-ui/logics/common/index.js +++ b/src-ui/logics/common/index.js @@ -1,9 +1,9 @@ +export { useSoftwareVersion } from "./useSoftwareVersion"; export { useComputeMode } from "./useComputeMode"; export { useInitProgress } from "./useInitProgress"; export { useIsBackendReady } from "./useIsBackendReady"; export { useWindow } from "./useWindow"; export { useIsOpenedConfigPage } from "./useIsOpenedConfigPage"; -export { useIsSoftwareUpdateAvailable } from "./useIsSoftwareUpdateAvailable"; export { useIsSoftwareUpdating } from "./useIsSoftwareUpdating"; export { useNotificationStatus } from "./useNotificationStatus"; export { useOpenFolder } from "./useOpenFolder"; @@ -11,4 +11,5 @@ export { useMessage } from "./useMessage"; export { useUpdateSoftware } from "./useUpdateSoftware"; export { useVolume } from "./useVolume"; export { useHandleNetworkConnection } from "./useHandleNetworkConnection"; -export { useIsVrctAvailable } from "./useIsVrctAvailable"; \ No newline at end of file +export { useIsVrctAvailable } from "./useIsVrctAvailable"; +export { useFetch } from "./useFetch"; \ No newline at end of file diff --git a/src-ui/logics/common/useFetch.js b/src-ui/logics/common/useFetch.js new file mode 100644 index 00000000..12ba5def --- /dev/null +++ b/src-ui/logics/common/useFetch.js @@ -0,0 +1,21 @@ +import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; + +export const useFetch = () => { + const asyncTauriFetchGithub = async (url) => { + console.log("tauriFetch", url); + + const release_response = await tauriFetch(url, { + method: "GET", + responseType: ResponseType.Json, + headers: { + "Accept": "application/vnd.github+json", + "User-Agent": "VRCTPluginApp" + } + }); + return release_response; + }; + + return { + asyncTauriFetchGithub, + }; +}; \ No newline at end of file diff --git a/src-ui/logics/common/useIsSoftwareUpdateAvailable.js b/src-ui/logics/common/useIsSoftwareUpdateAvailable.js deleted file mode 100644 index 569a3949..00000000 --- a/src-ui/logics/common/useIsSoftwareUpdateAvailable.js +++ /dev/null @@ -1,10 +0,0 @@ -import { useStore_IsSoftwareUpdateAvailable } from "@store"; - -export const useIsSoftwareUpdateAvailable = () => { - const { currentIsSoftwareUpdateAvailable, updateIsSoftwareUpdateAvailable } = useStore_IsSoftwareUpdateAvailable(); - - return { - currentIsSoftwareUpdateAvailable, - updateIsSoftwareUpdateAvailable, - }; -}; \ No newline at end of file diff --git a/src-ui/logics/common/useSoftwareVersion.js b/src-ui/logics/common/useSoftwareVersion.js new file mode 100644 index 00000000..18f24a7f --- /dev/null +++ b/src-ui/logics/common/useSoftwareVersion.js @@ -0,0 +1,40 @@ +import semver from "semver"; + +import { useStore_SoftwareVersion, useStore_LatestSoftwareVersionInfo } from "@store"; +import { useStdoutToPython } from "@logics/useStdoutToPython"; + +export const useSoftwareVersion = () => { + const { asyncStdoutToPython } = useStdoutToPython(); + const { currentLatestSoftwareVersionInfo, updateLatestSoftwareVersionInfo } = useStore_LatestSoftwareVersionInfo(); + const { currentSoftwareVersion, updateSoftwareVersion, pendingSoftwareVersion } = useStore_SoftwareVersion(); + + const getSoftwareVersion = () => { + pendingSoftwareVersion(); + asyncStdoutToPython("/get/data/version"); + }; + + const isPluginCompatible = (main_version, lower_version, upper_version) => { + // lower_version 以上かつ upper_version 以下なら互換性ありと判定 + return semver.gte(main_version, lower_version) && semver.lte(main_version, upper_version); + }; + + const checkVrctVerCompatibility = (min_version, max_version) => { + const current_vrct_version = currentSoftwareVersion.data; + const latest_vrct_version = currentLatestSoftwareVersionInfo.data.new_version; + + const is_plugin_supported = isPluginCompatible(current_vrct_version, min_version, max_version); + const is_plugin_supported_latest_vrct = isPluginCompatible(latest_vrct_version, min_version, max_version); + return { is_plugin_supported, is_plugin_supported_latest_vrct }; + }; + + return { + currentSoftwareVersion, + getSoftwareVersion, + updateSoftwareVersion, + + currentLatestSoftwareVersionInfo, + updateLatestSoftwareVersionInfo, + + checkVrctVerCompatibility, + }; +}; \ No newline at end of file diff --git a/src-ui/logics/configs/index.js b/src-ui/logics/configs/index.js index 22439030..912e2a7d 100644 --- a/src-ui/logics/configs/index.js +++ b/src-ui/logics/configs/index.js @@ -60,6 +60,6 @@ export { useOscPort } from "./advanced_settings/useOscPort"; export { useSupporters } from "./supporters/useSupporters"; +export { usePlugins } from "./plugins/usePlugins"; -export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition"; -export { useSoftwareVersion } from "./useSoftwareVersion"; \ No newline at end of file +export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition"; \ No newline at end of file diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js new file mode 100644 index 00000000..8bd533e0 --- /dev/null +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -0,0 +1,451 @@ +import { invoke } from "@tauri-apps/api/tauri"; +import { useTranslation } from "react-i18next"; +import { IS_PLUGIN_PATH_DEV_MODE, getPluginsList } from "@ui_configs"; +import { + store, + + createAtomWithHook, + useStore_SavedPluginsStatus, + useStore_PluginsData, + + useStore_FetchedPluginsInfo, + useStore_LoadedPlugins, +} from "@store"; +import { useStdoutToPython } from "@logics/useStdoutToPython"; + +import { transform } from "@babel/standalone"; +import { writeFile, createDir, exists, removeDir, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; +import { dev_plugins } from "@plugins_index"; +const imported_dev_plugins = []; +dev_plugins.forEach(async ({entry_path}) => { + imported_dev_plugins.push({ + index: await import(`@plugins_path/${entry_path}/index.jsx`), + downloaded_plugin_info: await import(`@plugins_path/${entry_path}/plugin_info.json`), + }); +}) + +import JSZip from "jszip"; + +import { useFetch, useSoftwareVersion, useNotificationStatus } from "@logics_common"; + +import * as logics_configs from "@logics_configs"; +import * as logics_main from "@logics_main"; +import * as logics_common from "@logics_common"; + +// PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL +const PLUGIN_LIST_URL = getPluginsList(); + +export const usePlugins = () => { + const { t } = useTranslation(); + const { showNotification_Success, showNotification_Error } = useNotificationStatus(); + const { asyncStdoutToPython } = useStdoutToPython(); + + const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo, errorFetchedPluginsInfo } = useStore_FetchedPluginsInfo(); + const { currentLoadedPlugins, updateLoadedPlugins, pendingLoadedPlugins } = useStore_LoadedPlugins(); + + const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); + const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); + const { checkVrctVerCompatibility } = useSoftwareVersion(); + + const { asyncTauriFetchGithub } = useFetch(); + + + const { i18n } = useTranslation(); + + const generatePluginContext = (downloaded_plugin_info) => { + const plugin_context = { + registerComponent: (component) => { + if (!downloaded_plugin_info.plugin_id || !downloaded_plugin_info.location || !component) { + return console.error("An invalid plugin was detected.", downloaded_plugin_info.plugin_id, downloaded_plugin_info.location, component); + } + + updateLoadedPlugins(prev => { + const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); + prev_map.set(downloaded_plugin_info.plugin_id, { + ...downloaded_plugin_info, + component: component, + }); + return Array.from(prev_map.values()); + }); + + }, + createAtomWithHook: (...args) => createAtomWithHook(...args), + logics: { ...logics_common, ...logics_configs, ...logics_main }, + i18n: i18n, + }; + return plugin_context; + } + + const asyncLoadPlugin = async (plugin_folder_relative_path) => { + const init_path = "plugins/" + plugin_folder_relative_path + "/index.esm.js"; + const downloaded_plugin_info_path = "plugins/" + plugin_folder_relative_path + "/plugin_info.json"; + const plugin_css_path = "plugins/" + plugin_folder_relative_path + "/main.css"; + try { + const downloaded_plugin_info_json = await readTextFile(downloaded_plugin_info_path, { dir: BaseDirectory.Resource, recursive: true }); + const downloaded_plugin_info = JSON.parse(downloaded_plugin_info_json); + + const plugin_code = await readTextFile(init_path, { dir: BaseDirectory.Resource, recursive: true }); + const cleaned_code = removeImportStatements(plugin_code); + const transpiled_code = transform(cleaned_code, { + presets: [ + ["env", { modules: false }], + "react", + ], + sourceType: "module" + }).code; + const blob = new Blob([transpiled_code], { type: "text/javascript" }); + const blob_url = URL.createObjectURL(blob); + const plugin_module = await import(/* @vite-ignore */ blob_url); + URL.revokeObjectURL(blob_url); + + if (plugin_module && plugin_module.init) { + plugin_module.init(generatePluginContext(downloaded_plugin_info)); + } + await loadPluginCSS(plugin_css_path); + + } catch (error) { + console.error("Failed to load plugin from", plugin_folder_relative_path, error); + } + }; + + const asyncLoadAllPlugins = async () => { + if (IS_PLUGIN_PATH_DEV_MODE) { + imported_dev_plugins.forEach(({ index, downloaded_plugin_info }) => { + if (!index || !downloaded_plugin_info) { + console.error("Invalid development plugin detected", index, downloaded_plugin_info); + return; + } + const plugin_context = generatePluginContext(downloaded_plugin_info); + if (index.init) { + index.init(plugin_context); + } else { + console.error("Plugin missing init function", downloaded_plugin_info); + } + }); + } else { + const is_plugins_dir_exists = await exists("plugins", { dir: BaseDirectory.Resource }); + if (!is_plugins_dir_exists) return; + + try { + const plugin_entries = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true }); + const plugin_files = plugin_entries.filter(entry => entry.children && Array.isArray(entry.children)); + + for (const target_dir of plugin_files) { + const target_path = target_dir.name; + await asyncLoadPlugin(target_path); + } + } catch (error) { + console.error("Error loading plugins:", error); + } + } + }; + + const downloadAndExtractPlugin = async (plugin) => { + const latest_plugin_info = plugin.latest_plugin_info; + try { + const plugin_zip_url = await fetchLatestPluginZipUrl(latest_plugin_info); + console.log("start download", plugin_zip_url); + // Rust コマンド経由で ZIP をダウンロード + const base64_zip = await invoke("download_zip_asset", { url: plugin_zip_url }); + // base64_zip をデコードして Uint8Array に変換 + const binary_string = atob(base64_zip); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + + // JSZip で ZIP を解凍 + const zip = await JSZip.loadAsync(bytes); + + // 展開先ディレクトリのパス(例:"plugins/" とする) + const target_plugin_path = "plugins/" + latest_plugin_info.plugin_id; + // 既に存在する場合は削除してから新規作成 + if (await exists(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true })) { + await removeDir(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true }); + } + await createDir(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true }); + + const file_promises = []; + zip.forEach((relative_path, zip_entry) => { + // .git 以下のファイルはスキップ + if (relative_path.startsWith(".git") || relative_path.includes("/.git/")) { + return; + } + const file_path = target_plugin_path + "/" + relative_path; + if (zip_entry.dir) { + file_promises.push( + createDir(file_path, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => { + if (!err.message?.includes("already exists")) { + console.error("Failed to create directory:", file_path, err); + } + }) + ); + } else { + const dir_path = file_path.substring(0, file_path.lastIndexOf("/")); + const promise = createDir(dir_path, { dir: BaseDirectory.Resource, recursive: true }) + .catch((err) => { + if (!err.message?.includes("already exists")) { + console.error("Failed to create parent directory:", dir_path, err); + } + }) + .then(() => zip_entry.async("text")) + .then(async (file_data) => { + await writeFile(file_path, file_data, { dir: BaseDirectory.Resource, recursive: true }); + }); + file_promises.push(promise); + } + }); + + await Promise.all(file_promises); + console.log("Plugin downloaded successfully."); + + const index_file_relative_path = plugin.plugin_id; + await asyncLoadPlugin(index_file_relative_path); + + console.log("Plugin loaded successfully."); + } catch (error) { + console.error("Error downloading and extracting plugin:", error); + } + }; + + const fetchLatestPluginZipUrl = async (plugin) => { + const api_url = plugin.url; + const response = await asyncTauriFetchGithub(api_url); + if (response.status !== 200) { + throw new Error("Failed to fetch latest release info, status: " + response.status); + } + const release_info = response.data; + const asset = release_info.assets.find((a) => a.name === plugin.asset_name); + if (!asset) { + throw new Error(`Asset ${plugin.asset_name} not found in the latest release`); + } + return asset.browser_download_url; + }; + + + const asyncFetchPluginsInfo = async () => { + if (store.is_fetched_plugins_info_already) return; + store.is_fetched_plugins_info_already = true; + + try { + const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL); + if (response.status !== 200) { + throw new Error("Failed to fetch plugins list, status: " + response.status); + } + const plugins_data = response.data; + const updated_list = await Promise.all( + plugins_data.map(async (plugin_data) => { + try { + const plugin_info = await asyncFetchPluginInfo(plugin_data.url); + return plugin_info; + } catch (error) { + console.error("Error fetching plugin info for URL: ", plugin_data.url, error); + return { + title: plugin_data.title, + plugin_id: plugin_data.plugin_id || plugin_data.title, + is_error: true, + error_message: error.message, + url: plugin_data.url + }; + } + }) + ); + updateFetchedPluginsInfo(updated_list); + } catch (error) { + console.error("Error fetching plugin info list: ", error); + errorFetchedPluginsInfo(); + } + + store.is_initialized_fetched_plugin_info = true; + } + + const asyncFetchPluginInfo = async (plugin_info_asset_url) => { + + const release_response = await asyncTauriFetchGithub(plugin_info_asset_url); + if (release_response.status !== 200) { + throw new Error(`Failed to fetch release info from ${plugin_info_asset_url}`); + } + const plugin_info_json = release_response.data.assets.find(asset => asset.name === "plugin_info.json"); + if (!plugin_info_json) { + throw new Error("plugin_info.json not found in release assets"); + } + const plugin_info_json_response = await asyncTauriFetchGithub(plugin_info_json.browser_download_url); + if (plugin_info_json_response.status !== 200) { + throw new Error(`Failed to fetch plugin_info.json from ${plugin_info_json.browser_download_url}`); + } + const plugin_info = plugin_info_json_response.data; + + + const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(plugin_info.min_supported_vrct_version, plugin_info.max_supported_vrct_version); + + return { + ...plugin_info, + is_plugin_supported: is_plugin_supported, + is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, + url: plugin_info_asset_url, + }; + } + + const handlePendingPlugin = (target_plugin_id, is_pending) => { + updatePluginsData((old_value) => { + const new_value = old_value.data.map((d) => { + if (d.plugin_id === target_plugin_id) { + d.is_pending = is_pending; + } + return d; + }); + return new_value; + }); + }; + + const toggleSavedPluginsStatus = (target_plugin_id) => { + const is_exists = currentSavedPluginsStatus.data.some( + (d) => d.plugin_id === target_plugin_id + ); + let new_value = []; + if (is_exists) { + new_value = currentSavedPluginsStatus.data.map((d) => { + if (d.plugin_id === target_plugin_id) { + d.is_enabled = !d.is_enabled; + (d.is_enabled) + ? showNotification_Success(t("plugin_notifications.is_enabled")) + : showNotification_Success(t("plugin_notifications.is_disabled")); + } + return d; + }); + } else { + new_value.push(...currentSavedPluginsStatus.data); + new_value.push({ + plugin_id: target_plugin_id, + is_enabled: true, + }); + showNotification_Success(t("plugin_notifications.is_enabled")) + } + + // 「currentPluginsData.data」でis_downloadedがtrueのものだけ残す + new_value = new_value.filter((item) => + currentPluginsData.data.some( + (plugin) => plugin.plugin_id === item.plugin_id && plugin.is_downloaded + ) + ); + + setSavedPluginsStatus(new_value); + }; + + + // Init時の処理 非対応のものを無効化する際に、savedDPluginsStatusから不要なものを削除する処理が邪魔になるので該当コードを削除したバージョン。Init以外で使用する時にはリファクタが必要になる。 + const setTargetSavedPluginsStatus_Init = (target_plugin_id, is_enabled) => { + const is_exists = currentSavedPluginsStatus.data.some( + (d) => d.plugin_id === target_plugin_id + ); + let new_value = []; + if (is_exists) { + new_value = currentSavedPluginsStatus.data.map((d) => { + if (d.plugin_id === target_plugin_id) { + d.is_enabled = is_enabled; + } + return d; + }); + } else { + new_value.push(...currentSavedPluginsStatus.data); + new_value.push({ + plugin_id: target_plugin_id, + is_enabled: is_enabled, + }); + } + + setSavedPluginsStatus(new_value); + }; + + + const setSavedPluginsStatus = (plugins_status) => { + pendingSavedPluginsStatus(); + asyncStdoutToPython("/set/data/plugins_status", plugins_status); + }; + + // init時、currentPluginsDataからのデータではデータ更新が間に合わないので、currentSavedPluginsStatusから直接取得 + const isAnyPluginEnabled_Init = () => { + return currentSavedPluginsStatus.data.some(plugin => plugin.is_enabled); + }; + + const isAnyPluginEnabled = () => { + return currentPluginsData.data.some(plugin => plugin.is_enabled); + }; + + const enabledPluginsList = () => { + return currentPluginsData.data.filter(plugin => plugin.is_enabled); + } + + const updateTargetPluginData = (target_plugin_id, attribute, value) => { + updatePluginsData(prev => { + prev.data.forEach(plugin => { + if (plugin.plugin_id === target_plugin_id) { + plugin[attribute] = value; + } + }); + return prev.data; + }); + } + + + + return { + asyncFetchPluginsInfo, + + isAnyPluginEnabled_Init, + isAnyPluginEnabled, + enabledPluginsList, + + asyncLoadAllPlugins, + downloadAndExtractPlugin, + + currentSavedPluginsStatus, + updateSavedPluginsStatus, + + currentPluginsData, + updatePluginsData, + + updateTargetPluginData, + + currentFetchedPluginsInfo, + updateFetchedPluginsInfo, + + currentLoadedPlugins, + updateLoadedPlugins, + + toggleSavedPluginsStatus, + setTargetSavedPluginsStatus_Init, + setSavedPluginsStatus, + + handlePendingPlugin, + }; +}; + +const removeImportStatements = (code) => { + return code + .split("\n") + .filter(line => !line.match(/^import\s+.*['"]react['"]/)) + .join("\n"); +}; + +// import { readTextFile, BaseDirectory } from "@tauri-apps/api/fs"; + +const loadPluginCSS = async (plugin_css_path) => { + if (!await exists(plugin_css_path, { dir: BaseDirectory.Resource, recursive: true })) return; + try { + // プラグインフォルダのルートにある main.css を読み込む + const css_content = await readTextFile(plugin_css_path, { dir: BaseDirectory.Resource }); + + // style タグを作成して head に挿入する + const style_tag = document.createElement("style"); + style_tag.id = `plugin-css-${plugin_css_path.replace(/[^a-zA-Z0-9_-]/g, "")}`; + style_tag.textContent = css_content; + document.head.appendChild(style_tag); + console.log("Plugin CSS loaded for:", plugin_css_path); + } catch (error) { + console.error("Failed to load plugin CSS from", plugin_css_path, error); + } +}; + +export { loadPluginCSS }; diff --git a/src-ui/logics/configs/useSoftwareVersion.js b/src-ui/logics/configs/useSoftwareVersion.js deleted file mode 100644 index c81b81c2..00000000 --- a/src-ui/logics/configs/useSoftwareVersion.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useStore_SoftwareVersion } from "@store"; -import { useStdoutToPython } from "@logics/useStdoutToPython"; - -export const useSoftwareVersion = () => { - const { asyncStdoutToPython } = useStdoutToPython(); - const { currentSoftwareVersion, updateSoftwareVersion, pendingSoftwareVersion } = useStore_SoftwareVersion(); - - const getSoftwareVersion = () => { - pendingSoftwareVersion(); - asyncStdoutToPython("/get/data/version"); - }; - - return { - currentSoftwareVersion, - getSoftwareVersion, - updateSoftwareVersion, - }; -}; \ No newline at end of file diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index 8df5dc46..4a4f944c 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -8,13 +8,13 @@ import { useNotificationStatus, useHandleNetworkConnection, + useSoftwareVersion, useComputeMode, useInitProgress, useIsBackendReady, useWindow, useMessage, useVolume, - useIsSoftwareUpdateAvailable, } from "@logics_common"; import { @@ -26,7 +26,6 @@ import { } from "@logics_main"; import { - useSoftwareVersion, useEnableAutoMicSelect, useEnableAutoSpeakerSelect, useMicHostList, @@ -73,6 +72,7 @@ import { useOverlayShowOnlyTranslatedMessages, useEnableNotificationVrcSfx, useHotkeys, + usePlugins, useOscIpAddress, useOscPort, } from "@logics_configs"; @@ -104,7 +104,7 @@ export const useReceiveRoutes = () => { addSentMessageLog, addReceivedMessageLog, } = useMessage(); - const { updateIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable(); + const { updateLatestSoftwareVersionInfo } = useSoftwareVersion(); const { updateSoftwareVersion } = useSoftwareVersion(); const { updateEnableAutoMicSelect } = useEnableAutoMicSelect(); const { updateEnableAutoSpeakerSelect } = useEnableAutoSpeakerSelect(); @@ -176,6 +176,7 @@ export const useReceiveRoutes = () => { const { updateEnableNotificationVrcSfx } = useEnableNotificationVrcSfx(); const { updateHotkeys } = useHotkeys(); + const { updateSavedPluginsStatus } = usePlugins(); const { updateOscIpAddress } = useOscIpAddress(); const { updateOscPort } = useOscPort(); @@ -205,7 +206,12 @@ export const useReceiveRoutes = () => { "/set/data/main_window_geometry": () => {}, "/run/open_filepath_logs": () => console.log("Opened Directory, Message Logs"), "/run/open_filepath_config_file": () => console.log("Opened Directory, Config File"), - "/run/update_software_flag": updateIsSoftwareUpdateAvailable, + "/run/software_update_info": (payload) => { + updateLatestSoftwareVersionInfo(prev => ({ + is_update_available: payload.is_update_available, + new_version: payload.new_version || prev.data.new_version, + })); + }, "/run/connected_network": handleNetworkConnection, // Main Page @@ -488,6 +494,10 @@ export const useReceiveRoutes = () => { "/get/data/hotkeys": updateHotkeys, "/set/data/hotkeys": updateHotkeys, + // Plugins + "/get/data/plugins_status": updateSavedPluginsStatus, + "/set/data/plugins_status": updateSavedPluginsStatus, + // Advanced Settings "/get/data/osc_ip_address": updateOscIpAddress, "/set/data/osc_ip_address": updateOscIpAddress, diff --git a/src-ui/plugins/dev_plugin_subtitles/index.jsx b/src-ui/plugins/dev_plugin_subtitles/index.jsx new file mode 100644 index 00000000..fc496275 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/index.jsx @@ -0,0 +1,25 @@ +import { initStore, StoreContext } from "@plugin_store"; +import { initI18n } from "@initI18n"; +import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer"; +import { SubtitlesController } from "./subtitle_system_container/_controllers/SubtitlesController.jsx"; + +export const init = (plugin_context) => { + const { createAtomWithHook, i18n, logics } = plugin_context; + + initStore(createAtomWithHook); + initI18n(i18n); + + const EntryComponents = () => { + return ( + + + + + + ); + }; + + plugin_context.registerComponent(EntryComponents); +}; + +export default init; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/locales/en.yml b/src-ui/plugins/dev_plugin_subtitles/locales/en.yml new file mode 100644 index 00000000..6a5ae6e1 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/locales/en.yml @@ -0,0 +1,2 @@ +main_page: + title: "VRCT Subtitles" \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js b/src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js new file mode 100644 index 00000000..6e71957e --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js @@ -0,0 +1,11 @@ +import en from "./en.yml"; +import ja from "./ja.yml"; +import plugin_info from "../plugin_info.json"; + +export const initI18n = (i18n) => { + const ns = plugin_info.plugin_id; + + // addResourceBundle will merge into i18n’s store + i18n.addResourceBundle("en", ns, en, /* deep = */ true, /* overwrite = */ true); + i18n.addResourceBundle("ja", ns, ja, /* deep = */ true, /* overwrite = */ true); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/locales/ja.yml b/src-ui/plugins/dev_plugin_subtitles/locales/ja.yml new file mode 100644 index 00000000..3862be1e --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/locales/ja.yml @@ -0,0 +1,2 @@ +main_page: + title: "字幕プレイヤー" \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/locales/usePluginTranslation.jsx b/src-ui/plugins/dev_plugin_subtitles/locales/usePluginTranslation.jsx new file mode 100644 index 00000000..2e9e8d79 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/locales/usePluginTranslation.jsx @@ -0,0 +1,7 @@ +import { useTranslation } from "react-i18next"; +import plugin_info from "../plugin_info.json"; + +export const usePluginTranslation = () => { + const ns = plugin_info.plugin_id; + return useTranslation(ns); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/plugin_configs.js b/src-ui/plugins/dev_plugin_subtitles/plugin_configs.js new file mode 100644 index 00000000..36f116d1 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_configs.js @@ -0,0 +1,7 @@ +export const configs = { + alias: { + "@plugin_store": "store/store.js", + "@initI18n": "locales/initI18n.js", + "@usePluginTranslation": "locales/usePluginTranslation.jsx", + } +} \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/plugin_info.json b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json new file mode 100644 index 00000000..ec673298 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json @@ -0,0 +1,20 @@ +{ + "title": "VRCT Subtitles", + "desc": "No description", + "plugin_id": "vrct_plugin_subtitles", + "asset_name": "vrct_plugin_subtitles.zip", + "location": "main_section", + "plugin_version": "0.0.5", + "min_supported_vrct_version": "3.0.5", + "max_supported_vrct_version": "3.0.5", + "locales": { + "en": { + "title": "VRCT Subtitles", + "desc": "No description" + }, + "ja": { + "title": "VRCT 字幕表示機能", + "desc": "VRCTのオーバーレイ機能を使い、目の前に字幕としてテキストを表示する機能です。ワールドギミックの開始タイミングに合わせて字幕を設定し、同時に表示しているだけではあります。" + } + } +} \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/store/store.js b/src-ui/plugins/dev_plugin_subtitles/store/store.js new file mode 100644 index 00000000..6f338573 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/store/store.js @@ -0,0 +1,37 @@ +const store_hooks = {}; + +export const initStore = (createAtomWithHook) => { + Object.assign(store_hooks, { + useStore_IsSubtitlePlaying: createAtomWithHook(false, "IsSubtitlePlaying", { is_state_ok: true }).useHook, + useStore_SubtitlePlaybackMode: createAtomWithHook("relative", "SubtitlePlaybackMode", { is_state_ok: true }).useHook, + useStore_SubtitleAbsoluteTargetTime: createAtomWithHook({ + hour: "23", + minute: "00", + }, "SubtitleAbsoluteTargetTime", { is_state_ok: true }).useHook, + useStore_IsCuesScheduled: createAtomWithHook(false, "IsCuesScheduled", { is_state_ok: true }).useHook, + useStore_CountdownAdjustment: createAtomWithHook(0, "CountdownAdjustment", { is_state_ok: true }).useHook, + useStore_EffectiveCountdown: createAtomWithHook(null, "EffectiveCountdown", { is_state_ok: true }).useHook, + useStore_SubtitleCues: createAtomWithHook([], "SubtitleCues", { is_state_ok: true }).useHook, + + useStore_SubtitleTimers: createAtomWithHook([], "SubtitleTimers", { is_state_ok: true }).useHook, + useStore_SubtitleCountdownTimerId: createAtomWithHook([], "SubtitleCountdownTimerId", { is_state_ok: true }).useHook, + useStore_SubtitleFileName: createAtomWithHook("ファイルが選択されていません", "SubtitleFileName", { is_state_ok: true }).useHook, + }); +}; + +export const useStore = (hook_name) => { + if (!store_hooks[hook_name]) { + throw new Error(`Hook ${hook_name} is not initialized.`); + } + return store_hooks[hook_name](); +}; + + +// StoreContext.js +import React, { createContext, useContext } from "react"; + +export const StoreContext = createContext(null); + +export const useStoreContext = () => { + return useContext(StoreContext); +}; diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx new file mode 100644 index 00000000..64349850 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx @@ -0,0 +1,47 @@ +import styles from "./SubtitleSystemContainer.module.scss"; +import { InputFileContainer } from "./input_file_container/InputFileContainer"; +import { ModeSelectorContainer } from "./mode_selector_container/ModeSelectorContainer"; +import { PlayControlContainer } from "./play_control_container/PlayControlContainer"; +import { CountdownContainer } from "./countdown_container/CountdownContainer"; +import { SubtitlesListContainer } from "./subtitles_list_container/SubtitlesListContainer"; +import { usePluginTranslation } from "@usePluginTranslation"; + +export const SubtitleSystemContainer = () => { + const { t } = usePluginTranslation(); + // const [srtContent, setSrtContent] = useState(""); + // const [cues, setCues] = useState([]); + // const [isPlaying, setIsPlaying] = useState(false); + + // 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から) + // const [playbackMode, setPlaybackMode] = useState("relative"); + // 絶対モード用の再生開始時刻(ドロップダウンで選択、HH:MM) + // const [targetHour, setTargetHour] = useState("23"); + // const [targetMinute, setTargetMinute] = useState("00"); + + // カウントダウン状態 + // // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数 + // const [initialCountdown, setInitialCountdown] = useState(null); + // countdownAdjustment: ユーザーが上下ボタンで調整する値(秒単位) + // const [countdownAdjustment, setCountdownAdjustment] = useState(0); + // effectiveCountdown: (initialCountdown + countdownAdjustment) から経過秒数を差し引いた表示用の値 + // const [effectiveCountdown, setEffectiveCountdown] = useState(null); + // cuesScheduled: 字幕タイマーが一度スケジュールされたか + // const [cuesScheduled, setCuesScheduled] = useState(false); + + // // タイマー(setTimeout/setInterval)のID管理用 + // const timersRef = useRef([]); + // // カウントダウンタイマー専用の ref + // const countdownIntervalRef = useRef(null); + + return ( +
+

{t("main_page.title")}

+ + + +
+ + +
+ ); +}; diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.module.scss new file mode 100644 index 00000000..ef618213 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.module.scss @@ -0,0 +1,124 @@ +.container { + padding: 2rem 4rem; + background: var(--dark_900_color); + border-radius: 1rem; + flex-shrink: 0; + height: 100%; + overflow: auto; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.title { + font-size: 1.8rem; + text-align: center; + flex-shrink: 0; +} + +.border { + width: 100%; + height: 0.2rem; + background-color: var(--dark_800_color); + flex-shrink: 0; + +} + + // label { + // display: block; + // font-size: 1.6rem; + // margin-bottom: 0.5rem; + // } + + // input, + // select { + // font-size: 1.6rem; + // padding: 0.5rem; + // border-radius: 0.5rem; + // border: 0.1rem solid #ccc; + // background: #333; + // color: #fff; + // } + + + // ボタンの基本スタイル + // button { + // // font-size: 1.8rem; + // // padding: 1rem 2rem; + // // border: none; + // // border-radius: 0.5rem; + // // cursor: pointer; + // // // transition: background 0.3s; + // // margin-right: 1rem; + + // &:focus { + // outline: none; + // box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + // } + // } + + // 再生開始用ボタン(通常時) + .primary { + background: #007bff; + color: #fff; + &:hover { + background: #0056b3; + } + } + + // 再生停止用ボタン + .secondary { + background: #dc3545; + color: #fff; + &:hover { + background: #a71d2a; + } + } + + + + // 「再生中」状態(クリック不可)用のスタイル + .is_playing { + background: #6c757d; + cursor: not-allowed; + pointer-events: none; + } + + + // 字幕一覧のテーブル + table { + width: 100%; + border-collapse: collapse; + margin-top: 2rem; + + th, + td { + padding: 1rem; + border: 0.1rem solid #444; + text-align: left; + font-size: 1.4rem; + } + + th { + background: #555; + } + + tbody { + tr { + cursor: pointer; + transition: background 0.2s; + + &:nth-child(even) { + background: #2a2a2a; + } + + &:hover { + background: #444; + } + } + } +} + +.subtitle_lists { + font-size: 1.4rem; +} diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_controllers/SubtitlesController.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_controllers/SubtitlesController.jsx new file mode 100644 index 00000000..d58be5aa --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_controllers/SubtitlesController.jsx @@ -0,0 +1,42 @@ +import { useStoreContext } from "../../store/store.js"; + +import { useSubtitles } from "../_logics/useSubtitles"; +import { secToDayTime } from "../_subtitles_utils" +import { useEffect } from "react"; +export const SubtitlesController = () => { + const { useSendTextToOverlay } = useStoreContext(); + const { sendTextToOverlay } = useSendTextToOverlay(); + + const { + currentIsSubtitlePlaying, + currentIsCuesScheduled, + updateIsCuesScheduled, + currentCountdownAdjustment, + currentEffectiveCountdown, + scheduleCues, + } = useSubtitles(); + + // currentEffectiveCountdown.data が 0 になったとき、字幕開始 + useEffect(() => { + if ( + currentIsSubtitlePlaying.data && + currentEffectiveCountdown.data !== null && + currentEffectiveCountdown.data <= 0 && + !currentIsCuesScheduled.data + ) { + sendTextToOverlay("スタート!"); + console.log("スタート!"); + // 調整後のタイミングで字幕スケジュールを開始 + scheduleCues(0); + updateIsCuesScheduled(true); + } + + if (currentEffectiveCountdown.data > 0) { + console.log(secToDayTime(currentEffectiveCountdown.data)); + sendTextToOverlay(secToDayTime(currentEffectiveCountdown.data)); + } + + }, [currentEffectiveCountdown.data, currentIsSubtitlePlaying.data, currentIsCuesScheduled.data, currentCountdownAdjustment.data]); + + return null; +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_logics/useSubtitles.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_logics/useSubtitles.jsx new file mode 100644 index 00000000..80585abf --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_logics/useSubtitles.jsx @@ -0,0 +1,182 @@ +import { useStore, useStoreContext } from "../../store/store.js"; + +export const useSubtitles = () => { + const { useSendTextToOverlay } = useStoreContext(); + const { sendTextToOverlay } = useSendTextToOverlay(); + + const { currentSubtitleFileName, updateSubtitleFileName } = useStore("useStore_SubtitleFileName"); + const { currentIsSubtitlePlaying, updateIsSubtitlePlaying } = useStore("useStore_IsSubtitlePlaying"); + const { currentSubtitlePlaybackMode, updateSubtitlePlaybackMode } = useStore("useStore_SubtitlePlaybackMode"); + const { currentSubtitleAbsoluteTargetTime, updateSubtitleAbsoluteTargetTime } = useStore("useStore_SubtitleAbsoluteTargetTime"); + const { currentIsCuesScheduled, updateIsCuesScheduled } = useStore("useStore_IsCuesScheduled"); + + const { currentCountdownAdjustment, updateCountdownAdjustment } = useStore("useStore_CountdownAdjustment"); + const { currentEffectiveCountdown, updateEffectiveCountdown } = useStore("useStore_EffectiveCountdown"); + const { currentSubtitleCues, updateSubtitleCues } = useStore("useStore_SubtitleCues"); + + // タイマー(setTimeout/setInterval)のID管理用 + const { currentSubtitleTimers, updateSubtitleTimers, addSubtitleTimers } = useStore("useStore_SubtitleTimers"); + // const timersRef = useRef([]); + // カウントダウンタイマー専用の ref + const { currentSubtitleCountdownTimerId, updateSubtitleCountdownTimerId, AddSubtitleCountdownTimerId } = useStore("useStore_SubtitleCountdownTimerId"); + + // cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる) + const scheduleCues = (offset) => { + // 字幕開始時の処理 + const startFunction = (cue) => { + let send_text = ""; + if (cue.actor !== "") { + send_text = `[${cue.actor}] ${cue.text}`; + } else { + send_text = `${cue.text}`; + } + console.log(`字幕開始 (index: ${cue.index}) send_text:${send_text}`); + sendTextToOverlay(send_text); + }; + + // 字幕終了時の処理 + const endFunction = (cue) => { + console.log(`字幕終了 (index: ${cue.index}): ${cue.text}`); + // 必要に応じた終了処理(例:テキストクリア)を実装可能 + // sendTextToOverlay(""); + }; + + currentSubtitleCues.data.forEach((cue) => { + const startDelay = cue.startTime * 1000 + offset; + const endDelay = cue.endTime * 1000 + offset; + if (startDelay >= 0) { + const timerId = setTimeout(() => startFunction(cue), startDelay); + addSubtitleTimers(timerId); + } + if (endDelay >= 0) { + const timerId = setTimeout(() => endFunction(cue), endDelay); + addSubtitleTimers(timerId); + } + }); + }; + + + // カウントダウンタイマーの開始/再登録(指定した値から1秒ごとに減らす) + const startCountdownInterval = (startValue) => { + // 既存のタイマーがあればクリア + if (currentSubtitleCountdownTimerId.data) { + clearInterval(currentSubtitleCountdownTimerId.data); + } + // 新たな開始値を設定 + updateEffectiveCountdown(startValue); + const countdown_timer_id = setInterval(() => { + updateEffectiveCountdown((prev) => { + if (prev.data <= 1) { + clearInterval(currentSubtitleCountdownTimerId.data); + return 0; + } + return prev.data - 1; + }); + }, 1000); + updateSubtitleCountdownTimerId(countdown_timer_id); + addSubtitleTimers(currentSubtitleCountdownTimerId.data); + }; + + + // 字幕一覧の表示(relative モードの場合、クリックでジャンプ) + // テーブル内の字幕行をクリック(relative モードのみ)でジャンプ + const handleJump = (jumpCue) => { + if (currentSubtitlePlaybackMode.data !== "relative") return; + handleSubtitlesStop(); + const offset = -jumpCue.startTime * 1000; + scheduleCues(offset); + updateIsSubtitlePlaying(true); + }; + + + + // 「再生開始」ボタン押下時の処理 + const handleSubtitlesStart = () => { + handleSubtitlesStop(); + updateIsSubtitlePlaying(true); + updateIsCuesScheduled(false); + const target_time = currentSubtitleAbsoluteTargetTime.data; + + let computedCountdown = 0; + if (currentSubtitlePlaybackMode.data === "absolute") { + const now = new Date(); + const hour = parseInt(target_time.hour, 10); + const minute = parseInt(target_time.minute, 10); + let targetDate = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + hour, + minute, + 0, + 0 + ); + if (targetDate.getTime() < now.getTime()) { + targetDate.setDate(targetDate.getDate() + 1); + } + computedCountdown = Math.ceil((targetDate.getTime() - now.getTime()) / 1000); + } else { + computedCountdown = 10; // relative モードの場合は固定値 + } + // setInitialCountdown(computedCountdown); + // 調整値を反映した開始値 + const startValue = computedCountdown + currentCountdownAdjustment.data; + startCountdownInterval(startValue); + sendTextToOverlay(startValue.toString()); + }; + + + // すべてのタイマーを停止し、各状態を初期化する + const handleSubtitlesStop = () => { + currentSubtitleTimers.data.forEach((timerId) => { + clearTimeout(timerId); + clearInterval(timerId); + }); + + updateSubtitleTimers([]); + if (currentSubtitleCountdownTimerId.data) { + clearInterval(currentSubtitleCountdownTimerId.data); + updateSubtitleCountdownTimerId(null); + } + console.log("再生を停止しました。"); + updateIsSubtitlePlaying(false); + // setInitialCountdown(null); + updateEffectiveCountdown(null); + updateCountdownAdjustment(0); + updateIsCuesScheduled(false); + }; + + + return { + currentSubtitleFileName, + updateSubtitleFileName, + + currentIsSubtitlePlaying, + updateIsSubtitlePlaying, + + currentSubtitlePlaybackMode, + updateSubtitlePlaybackMode, + + currentSubtitleAbsoluteTargetTime, + updateSubtitleAbsoluteTargetTime, + + currentIsCuesScheduled, + updateIsCuesScheduled, + + currentCountdownAdjustment, + updateCountdownAdjustment, + + currentEffectiveCountdown, + updateEffectiveCountdown, + + currentSubtitleCues, + updateSubtitleCues, + + handleSubtitlesStart, + handleSubtitlesStop, + startCountdownInterval, + scheduleCues, + handleJump, + } + +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_subtitles_utils.js b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_subtitles_utils.js new file mode 100644 index 00000000..8bed1f7b --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_subtitles_utils.js @@ -0,0 +1,112 @@ + +/** + * SRT形式の文字列を解析する関数 + * 改行コードを正規化し、空行で分割して解析する + * (actor は存在しないため、空文字列をセット) + */ +export const parseSRT = (data) => { + const cues = []; + const normalizedData = data.replace(/\r\n/g, "\n").trim(); + const blocks = normalizedData.split(/\n\s*\n/); + blocks.forEach((block) => { + const lines = block.split("\n").filter((line) => line.trim() !== ""); + if (lines.length >= 3) { + const index = parseInt(lines[0], 10); + const timeMatch = lines[1].match(/([\d:,]+)\s+-->\s+([\d:,]+)/); + if (!timeMatch) return; + const start = parseTime(timeMatch[1]); + const end = parseTime(timeMatch[2]); + const text = lines.slice(2).join("\n"); + cues.push({ index, startTime: start, endTime: end, actor: "", text }); + } + }); + return cues; +}; + +/** + * ASS形式の文字列を解析する関数 + * [Events] セクション内の "Dialogue:" 行から、 + * フォーマット "Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text" + * に沿って分割する。 + * ここでは Name を actor、Text を text として抽出する。 + */ +export const parseASS = (data) => { + const cues = []; + const lines = data.split(/\r?\n/); + let index = 1; + lines.forEach((line) => { + if (line.startsWith("Dialogue:")) { + const dialogueLine = line.substring("Dialogue:".length).trim(); + const parts = dialogueLine.split(","); + // parts[0]: Layer, parts[1]: Start, parts[2]: End, parts[3]: Style, parts[4]: Name, parts[5]: MarginL, parts[6]: MarginR, parts[7]: MarginV, parts[8]: Effect, parts[9]~: Text + if (parts.length < 10) return; + const startTime = parseASSTime(parts[1].trim()); + const endTime = parseASSTime(parts[2].trim()); + const actor = parts[4].trim(); + const text = parts.slice(9).join(",").trim(); + cues.push({ index: index++, startTime, endTime, actor, text }); + } + }); + return cues; +}; + +/** + * "H:MM:SS.cc" 形式の ASS 時刻文字列を秒数に変換する関数 + * 例: "0:00:10.52" → 10.52 秒 + */ +export const parseASSTime = (timeString) => { + const parts = timeString.split(":"); + if (parts.length !== 3) return 0; + const hours = parseFloat(parts[0]); + const minutes = parseFloat(parts[1]); + const seconds = parseFloat(parts[2]); + return hours * 3600 + minutes * 60 + seconds; +}; + +/** + * "HH:MM:SS,mmm" 形式の SRT 時刻文字列を秒数に変換する関数 + */ +export const parseTime = (timeString) => { + const [hms, ms] = timeString.split(","); + const [hours, minutes, seconds] = hms.split(":").map(Number); + return hours * 3600 + minutes * 60 + seconds + Number(ms) / 1000; +}; + +const padTime = (int) => { + return String(int).padStart(2, "0"); +}; + +export const secToDayTime = (seconds) => { + const day = Math.floor(seconds / 86400); + const hour = Math.floor((seconds % 86400) / 3600); + const min = Math.floor((seconds % 3600) / 60); + const sec = seconds % 60; + let time = ""; + // day が 0 の場合は「日」は出力しない(hour や min も同様) + if (day !== 0) { + time = `${day}日${hour}時間${min}分${sec}秒`; + } else if (hour !== 0) { + time = `${padTime(hour)}:${padTime(min)}:${padTime(sec)}`; + } else { + time = `${padTime(min)}:${padTime(sec)}`; + } + // } else { + // time = `${padTime(sec)}`; + // } + return time; +}; + + +// HH:MM:SS 形式に変換する補助関数 +export const formatTime = (timeInSeconds) => { + const hours = Math.floor(timeInSeconds / 3600); + const minutes = Math.floor((timeInSeconds % 3600) / 60); + const seconds = Math.floor(timeInSeconds % 60); + return ( + String(hours).padStart(2, "0") + + ":" + + String(minutes).padStart(2, "0") + + ":" + + String(seconds).padStart(2, "0") + ); +}; diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.jsx new file mode 100644 index 00000000..419db219 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.jsx @@ -0,0 +1,72 @@ +import React, { useState, useRef, useEffect } from "react"; +import styles from "./CountdownContainer.module.scss"; +import { secToDayTime } from "../_subtitles_utils"; +import { useSubtitles } from "../_logics/useSubtitles"; + +export const CountdownContainer = () => { + const { + updateCountdownAdjustment, + currentEffectiveCountdown, + currentIsCuesScheduled, + startCountdownInterval, + } = useSubtitles(); + // カウントダウン表示:字幕開始前は常に表示 + + // if (currentEffectiveCountdown.data === 0) return null; + if (currentEffectiveCountdown.data === null && currentIsCuesScheduled.data) return null; + + return ( +
+ カウントダウン: {secToDayTime(currentEffectiveCountdown.data)} +
+ {/* 1分単位の調整ボタン */} +
+ + +
+
+ {/* 1秒単位の調整ボタン */} +
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.module.scss new file mode 100644 index 00000000..0ad5a7ff --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.module.scss @@ -0,0 +1,47 @@ +.container { + margin-top: 1rem; + font-size: 2.6rem; + display: flex; + align-items: center; + gap: 1rem; + flex-direction: column; + + span { + font-weight: bold; + } + +} + +.adjust_button_container { + display: flex; + gap: 10rem; +} + +.adjust_button_wrapper { + display: flex; + flex-direction: column; + gap: 4rem; +} + + + +.adjust_button { + padding: 1rem 1.4rem; + font-size: 1.8rem; + border-radius: 0.4rem; + background: var(--primary_600_color); + color: #fff; + cursor: pointer; + + &:hover { + background: var(--primary_400_color); + } + &:active { + background: var(--primary_650_color); + } +} + +.adjust_button_border { + background-color: var(--dark_600_color); + width: 0.2rem; +} \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.jsx new file mode 100644 index 00000000..1e234765 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.jsx @@ -0,0 +1,62 @@ +import React, { useState, useRef, useEffect } from "react"; +import styles from "./InputFileContainer.module.scss"; +import { useSubtitles } from "../_logics/useSubtitles"; +import { parseSRT, parseASS } from "../_subtitles_utils"; + +export const InputFileContainer = () => { + const { + updateSubtitleFileName, + currentSubtitleFileName, + updateSubtitleCues, + handleSubtitlesStop + } = useSubtitles(); + + // ファイルアップロード時の処理 + const handleFileUpload = (event) => { + const file = event.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target.result; + let parsedCues = []; + // 拡張子により ASS と SRT を判定 + if (file.name.toLowerCase().endsWith(".ass")) { + parsedCues = parseASS(content); + } else { + parsedCues = parseSRT(content); + } + updateSubtitleCues(parsedCues); + console.log("Parsed cues:", parsedCues); + updateSubtitleFileName(file.name); + + }; + reader.readAsText(file); + }; + + + // ファイルクリア + const handleClearFile = () => { + handleSubtitlesStop(); + updateSubtitleFileName("ファイルが選択されていません"); + updateSubtitleCues([]); + }; + + return ( +
+
+ + +

{currentSubtitleFileName.data}

+
+ +
+ ); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.module.scss new file mode 100644 index 00000000..91966cbb --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.module.scss @@ -0,0 +1,42 @@ +.container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6rem; +} + +.input_file_wrapper { + display: flex; + align-items: center; + gap: 1rem; +} +.input_file_label { + font-size: 1.4rem; + border-radius: 0.4rem; + padding: 1rem; + background-color: var(--dark_850_color); + border: 0.1rem solid var(--dark_400_color); + flex-shrink: 0; +} +.input_file_i { + display: none; +} +.file_name { + font-size: 1.6rem; + padding: 0.5rem; + width: 100%; + max-width: 60rem; +} + +.file_clear { + background: var(--dark_800_color); + color: var(--dark_200_color); + border-radius: 0.4rem; + font-size: 1.2rem; + padding: 0.6rem 1rem; + cursor: pointer; + + &:hover { + background: var(--error_bc_color); + } +} \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx new file mode 100644 index 00000000..e19ddba2 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx @@ -0,0 +1,74 @@ +import styles from "./ModeSelectorContainer.module.scss"; +import { useSubtitles } from "../_logics/useSubtitles"; +export const ModeSelectorContainer = () => { + const { + currentSubtitlePlaybackMode, + updateSubtitlePlaybackMode, + currentSubtitleAbsoluteTargetTime, + updateSubtitleAbsoluteTargetTime, + } = useSubtitles(); + + const target_time = currentSubtitleAbsoluteTargetTime.data; + + const handleOnchangeTargetTime = (key, value) => { + updateSubtitleAbsoluteTargetTime((old_value) => { + return { + ...old_value.data, + [key]: value, + } + }); + }; + + + return ( +
+
+ +
+ + {currentSubtitlePlaybackMode.data === "absolute" && ( +
+ +
+ + : + +
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss new file mode 100644 index 00000000..a3c805d9 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss @@ -0,0 +1,50 @@ +.container { + // background-color: red; + display: flex; + gap: 4rem; +} +.mode_selector_wrapper { + display: flex; + align-items: center; + gap: 2rem; +} +.absolute_time_label { + font-size: 1.4rem; +} + +.mode_selector { + font-size: 1.6rem; + padding: 0.6rem 1rem; + border-radius: 0.5rem; + border: 0.1rem solid var(--dark_400_color); + cursor: pointer; +} + +.mode_selector_item { + background-color: var(--dark_800_color); +} + + +.time_section { + display: flex; + gap: 2rem; + justify-content: center; + align-items: center; +} + +.time_selects { + display: flex; + align-items: center; + gap: 0.5rem; +} + + +.time_selects_item { + width: 6rem; + text-align: center; + padding: 0.6rem 1rem; + font-size: 1.8rem; + background-color: var(--dark_850_color); + border: 0.1rem solid var(--dark_400_color); + cursor: pointer; +} \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.jsx new file mode 100644 index 00000000..05e45e01 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.jsx @@ -0,0 +1,33 @@ +// import React, { useState, useRef, useEffect } from "react"; +import styles from "./PlayControlContainer.module.scss"; +import { useSubtitles } from "../_logics/useSubtitles"; +import clsx from "clsx"; + +export const PlayControlContainer = () => { + const { + currentIsSubtitlePlaying, + handleSubtitlesStart, + handleSubtitlesStop, + } = useSubtitles(); + + const is_playing = currentIsSubtitlePlaying.data; + + const playback_button_classname = clsx(styles.playback_button, { + [styles.is_playing]: is_playing, + }); + return ( +
+ + {is_playing && + + } +
+ ); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.module.scss new file mode 100644 index 00000000..ea5de457 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.module.scss @@ -0,0 +1,31 @@ +.container { + display: flex; + gap: 4rem; + display: flex; + justify-content: center; +} + +.playback_button, .playback_stop_button { + font-size: 1.6rem; + padding: 1rem 2rem; + cursor: pointer; + border-radius: 0.4rem; +} + +.playback_button { + background-color: var(--primary_550_color); + &:hover { + background-color: var(--primary_400_color); + } + &.is_playing { + background-color: var(--primary_650_color); + pointer-events: none; + color: var(--dark_400_color); + } +} +.playback_stop_button { + background-color: var(--dark_800_color); + &:hover { + background-color: var(--error_bc_color); + } +} \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx new file mode 100644 index 00000000..b997870e --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx @@ -0,0 +1,45 @@ +import React, { useState, useRef, useEffect } from "react"; +import styles from "./SubtitlesListContainer.module.scss"; +import { useSubtitles } from "../_logics/useSubtitles"; +import { formatTime } from "../_subtitles_utils"; + +export const SubtitlesListContainer = () => { + const { currentSubtitleCues, handleJump } = useSubtitles(); + + if (currentSubtitleCues.data.length < 0 ) return null; + + return ( +
+

字幕一覧

+ + + + + + + + + + + + {currentSubtitleCues.data.map((cue) => ( + handleJump(cue)} + className={styles.tableRow} + > + + + + + + + ))} + +
番号開始終了Actorテキスト
{cue.index}{formatTime(cue.startTime)}{formatTime(cue.endTime)}{cue.actor}{cue.text}
+

+ ※ 行をクリックすると、その字幕の位置にジャンプします。(相対モードのみ) +

+
+ ); +}; \ No newline at end of file diff --git a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/src-ui/plugins/plugins_index.js b/src-ui/plugins/plugins_index.js new file mode 100644 index 00000000..ea240627 --- /dev/null +++ b/src-ui/plugins/plugins_index.js @@ -0,0 +1,3 @@ +export const dev_plugins = [ + { entry_path: "dev_plugin_subtitles" } +]; \ No newline at end of file diff --git a/src-ui/store.js b/src-ui/store.js index 4aa4a694..d1712f1c 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -20,6 +20,9 @@ export const store = { log_box_ref: null, text_area_ref: null, is_applied_init_message_box_height: false, + is_initialized_load_plugin: false, + is_fetched_plugins_info_already: false, + is_initialized_fetched_plugin_info: false, }; const generatePropertyNames = (base_name) => ({ @@ -34,7 +37,7 @@ const generatePropertyNames = (base_name) => ({ }); -const createAtomWithHook = (initialValue, base_name, options) => { +export const createAtomWithHook = (initialValue, base_name, options) => { const property_names = generatePropertyNames(base_name); const atomInstance = atom({ state: (options?.is_state_ok) ? "ok" : "pending", @@ -114,7 +117,10 @@ export const { atomInstance: Atom_MainFunctionsStateMemory, useHook: useStore_Ma transcription_receive: false, }, "MainFunctionsStateMemory"); export const { atomInstance: Atom_OpenedQuickSetting, useHook: useStore_OpenedQuickSetting } = createAtomWithHook("", "OpenedQuickSetting"); -export const { atomInstance: Atom_IsSoftwareUpdateAvailable, useHook: useStore_IsSoftwareUpdateAvailable } = createAtomWithHook(false, "IsSoftwareUpdateAvailable"); +export const { atomInstance: Atom_LatestSoftwareVersionInfo, useHook: useStore_LatestSoftwareVersionInfo } = createAtomWithHook({ + is_update_available: false, + new_version: "0.0.0", +}, "LatestSoftwareVersionInfo"); export const { atomInstance: Atom_InitProgress, useHook: useStore_InitProgress } = createAtomWithHook(0, "InitProgress"); export const { atomInstance: Atom_IsBreakPoint, useHook: useStore_IsBreakPoint } = createAtomWithHook(false, "IsBreakPoint"); export const { atomInstance: Atom_IsSoftwareUpdating, useHook: useStore_IsSoftwareUpdating } = createAtomWithHook(false, "IsSoftwareUpdating"); @@ -273,6 +279,12 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA toggle_transcription_receive: null, }, "Hotkeys"); +// Plugins +export const { atomInstance: Atom_FetchedPluginsInfo, useHook: useStore_FetchedPluginsInfo } = createAtomWithHook([], "FetchedPluginsInfo"); +export const { atomInstance: Atom_LoadedPlugins, useHook: useStore_LoadedPlugins } = createAtomWithHook([], "LoadedPlugins"); +export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([], "SavedPluginsStatus"); +export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } = createAtomWithHook([], "PluginsData"); + // Advanced Settings export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress"); export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createAtomWithHook("9000", "OscPort"); diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index 24111092..d6ef28eb 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -52,6 +52,21 @@ export const ui_configs = { ] }; +// true: src-ui\plugins false: src-tauri\target\debug\plugins +export const IS_PLUGIN_PATH_DEV_MODE = false; + +// true: dev_vrct_plugins_list.json false: vrct_plugins_list.json +export const IS_PLUGIN_LIST_URL_DEV_MODE = false; + +export const getPluginsList = () => { + const base_url = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/"; + const plugins_list_url = (IS_PLUGIN_LIST_URL_DEV_MODE) + ? base_url + "dev_vrct_plugins_list.json" + : base_url + "vrct_plugins_list.json"; + return plugins_list_url; +}; +if (IS_PLUGIN_PATH_DEV_MODE || IS_PLUGIN_LIST_URL_DEV_MODE) console.warn("ui_configs IS_PLUGIN_PATH_DEV_MODE or IS_PLUGIN_LIST_URL_DEV_MODE is true. Turn to 'false' when it's production environment."); + export const translator_status = [ { id: "DeepL", label: "DeepL", is_available: false }, { id: "DeepL_API", label: `DeepL API`, is_available: false }, diff --git a/vite.config.js b/vite.config.js index 0d44e3e2..78a27f37 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,62 +1,103 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import svgr from "vite-plugin-svgr"; +import yaml from "@rollup/plugin-yaml"; import path from "path"; +import { dev_plugins } from "./src-ui/plugins/plugins_index.js"; + + // https://vitejs.dev/config/ -export default defineConfig(async () => ({ - plugins: [react(), svgr()], - assetsInclude: ["**/*.yml"], +export default defineConfig(async () => { + const plugin_aliases = await getPluginAliases(); - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: 1420, - strictPort: true, - watch: { - // 3. tell vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], - }, - }, + return { + plugins: [ + yaml({ include: ["**/*.yml", "**/*.yaml"] }), + react(), + svgr(), + ], - build: { - outDir: path.resolve(__dirname, "dist"), - rollupOptions: { - input: { - main: path.resolve(__dirname, "index.html"), + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], }, }, - }, - resolve: { - alias: { - "@root": path.resolve(__dirname), - "@test_data": path.resolve(__dirname, "./test_data.js"), - - "@ui_configs": path.resolve(__dirname, "src-ui/ui_configs.js"), - "@scss_mixins": path.resolve(__dirname, "src-ui/common_css/mixins.scss"), - "@store": path.resolve(__dirname, "src-ui/store.js"), - "@images": path.resolve(__dirname, "src-ui/assets"), - "@utils": path.resolve(__dirname, "src-ui/utils.js"), - "@logics": path.resolve(__dirname, "src-ui/logics"), - "@logics_common": path.resolve(__dirname, "src-ui/logics/common"), - "@logics_main": path.resolve(__dirname, "src-ui/logics/main"), - "@logics_configs": path.resolve(__dirname, "src-ui/logics/configs"), - - "@setting_box": path.resolve(__dirname, "src-ui/app/config_page/setting_section/setting_box/index.js"), - "@common_components": path.resolve(__dirname, "src-ui/common_components/index.js"), + build: { + outDir: path.resolve(__dirname, "dist"), + rollupOptions: { + input: { + main: path.resolve(__dirname, "index.html"), + }, + }, + sourcemap: true, }, - }, - css: { - preprocessorOptions: { - scss: { - api: "modern-compiler" + resolve: { + alias: { + "@root": path.resolve(__dirname), + "@test_data": path.resolve(__dirname, "./test_data.js"), + + "@ui_configs": path.resolve(__dirname, "src-ui/ui_configs.js"), + "@scss_mixins": path.resolve(__dirname, "src-ui/common_css/mixins.scss"), + "@store": path.resolve(__dirname, "src-ui/store.js"), + "@images": path.resolve(__dirname, "src-ui/assets"), + "@utils": path.resolve(__dirname, "src-ui/utils.js"), + "@logics": path.resolve(__dirname, "src-ui/logics"), + "@logics_common": path.resolve(__dirname, "src-ui/logics/common"), + "@logics_main": path.resolve(__dirname, "src-ui/logics/main"), + "@logics_configs": path.resolve(__dirname, "src-ui/logics/configs"), + + "@setting_box": path.resolve(__dirname, "src-ui/app/config_page/setting_section/setting_box/index.js"), + "@common_components": path.resolve(__dirname, "src-ui/common_components/index.js"), + + // Plugins + "@plugins_path": path.resolve(__dirname, "src-ui/plugins"), + "@plugins_index": path.resolve(__dirname, "src-ui/plugins/plugins_index.js"), + ...plugin_aliases, + }, + }, + + css: { + preprocessorOptions: { + scss: { + api: "modern-compiler" + } } } - } + }; +}); -})); + + +// 各プラグインのエイリアスを動的に読み込む関数 +const getPluginAliases = async () => { + const aliases = {}; + // dev_plugins 配列の各プラグインについて処理する + for (const plugin of dev_plugins) { + const entry_path = plugin.entry_path; // 例: "dev_plugin_subtitles" + try { + // エイリアス設定ファイルは各プラグインフォルダ内の "configs.js" に記述されている前提 + const pluginConfig = await import(`./src-ui/plugins/${entry_path}/plugin_configs.js`); + if (pluginConfig.configs && pluginConfig.configs.alias) { + for (const [alias_key, alias_relative_path] of Object.entries(pluginConfig.configs.alias)) { + + // ホスト側の絶対パスに変換 + aliases[alias_key] = path.resolve(__dirname, "src-ui/plugins", entry_path, alias_relative_path); + } + } + } catch (error) { + console.error(`Error loading alias config for plugin ${plugin.plugin_info.plugin_id}:`, error); + } + } + return aliases; +}; \ No newline at end of file