From 65ee75b49e50deb669ffbe2b17fc614978ad2b5f Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:32:48 +0900 Subject: [PATCH 01/47] [Update/TMP] Add feature Scheduled message send. --- src-ui/app/main_page/MainPage.jsx | 2 + .../plugins_section/PluginsSection.jsx | 349 ++++++++++++++++++ .../PluginsSection.module.scss | 159 ++++++++ 3 files changed, 510 insertions(+) create mode 100644 src-ui/app/main_page/plugins_section/PluginsSection.jsx create mode 100644 src-ui/app/main_page/plugins_section/PluginsSection.module.scss diff --git a/src-ui/app/main_page/MainPage.jsx b/src-ui/app/main_page/MainPage.jsx index a83ef09a..324bc463 100644 --- a/src-ui/app/main_page/MainPage.jsx +++ b/src-ui/app/main_page/MainPage.jsx @@ -2,6 +2,7 @@ import clsx from "clsx"; import styles from "./MainPage.module.scss"; import { SidebarSection } from "./sidebar_section/SidebarSection"; import { MainSection } from "./main_section/MainSection"; +import { PluginsSection } from "./plugins_section/PluginsSection"; import { useIsOpenedConfigPage } from "@logics_common"; export const MainPage = () => { @@ -15,6 +16,7 @@ export const MainPage = () => {
+
); diff --git a/src-ui/app/main_page/plugins_section/PluginsSection.jsx b/src-ui/app/main_page/plugins_section/PluginsSection.jsx new file mode 100644 index 00000000..7925c4d5 --- /dev/null +++ b/src-ui/app/main_page/plugins_section/PluginsSection.jsx @@ -0,0 +1,349 @@ +import React, { useState, useRef, useEffect } from "react"; +import styles from "./PluginsSection.module.scss"; +import { useSendTextToOverlay } from "@logics_configs"; + +export const PluginsSection = () => { + const { sendTextToOverlay } = useSendTextToOverlay(); + 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("19"); + 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 fileInputRef = useRef(null); + + // ファイルアップロード処理 + const handleFileUpload = (event) => { + const file = event.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target.result; + setSrtContent(content); + const parsedCues = parseSRT(content); + setCues(parsedCues); + console.log("パース結果:", parsedCues); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + reader.readAsText(file); + }; + + // 字幕開始時の処理 + const startFunction = (cue) => { + console.log(`字幕開始 (index: ${cue.index}): ${cue.text}`); + sendTextToOverlay(cue.text); + }; + + // 字幕終了時の処理 + const endFunction = (cue) => { + console.log(`字幕終了 (index: ${cue.index}): ${cue.text}`); + // 必要に応じて終了処理(例:テキストクリア) + // sendTextToOverlay(""); + }; + + // すべてのタイマーを停止し、各状態を初期化する + const handleStop = () => { + timersRef.current.forEach((timerId) => { + clearTimeout(timerId); + clearInterval(timerId); + }); + timersRef.current = []; + console.log("再生を停止しました。"); + setIsPlaying(false); + setInitialCountdown(null); + setEffectiveCountdown(null); + setCountdownAdjustment(0); + setCuesScheduled(false); + }; + + // cues のスケジュールを行う(offset は countdownAdjustment * 1000) + const scheduleCues = (offset) => { + cues.forEach((cue) => { + const startDelay = cue.startTime * 1000 + offset; + const endDelay = cue.endTime * 1000 + offset; + if (startDelay >= 0) { + const timerId = setTimeout(() => startFunction(cue), startDelay); + timersRef.current.push(timerId); + } + if (endDelay >= 0) { + const timerId = setTimeout(() => endFunction(cue), endDelay); + timersRef.current.push(timerId); + } + }); + }; + + // カウントダウンタイマーの開始 + const startCountdownInterval = (initialValue) => { + // 初期表示は (initialValue + countdownAdjustment) + setEffectiveCountdown(initialValue + countdownAdjustment); + const countdownInterval = setInterval(() => { + setEffectiveCountdown((prev) => { + if (prev <= 1) { + clearInterval(countdownInterval); + return 0; + } + return prev - 1; + }); + }, 1000); + timersRef.current.push(countdownInterval); + }; + + // 「再生開始」ボタン押下時の処理 + const handleStart = () => { + handleStop(); + setIsPlaying(true); + setCuesScheduled(false); + + let computedCountdown = 0; + if (playbackMode === "absolute") { + const now = new Date(); + const hour = parseInt(targetHour, 10); + const minute = parseInt(targetMinute, 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); + setEffectiveCountdown(computedCountdown + countdownAdjustment); + sendTextToOverlay((computedCountdown + countdownAdjustment).toString()); + startCountdownInterval(computedCountdown); + }; + + // effectiveCountdown が 0 になったとき、字幕開始 + useEffect(() => { + if ( + isPlaying && + effectiveCountdown !== null && + effectiveCountdown <= 0 && + !cuesScheduled + ) { + sendTextToOverlay("Start."); + console.log("Start."); + scheduleCues(0); + setCuesScheduled(true); + } + }, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]); + + // テーブル内の字幕行をクリック(relative モードのみ)でジャンプ + const handleJump = (jumpCue) => { + if (playbackMode !== "relative") return; + handleStop(); + const offset = -jumpCue.startTime * 1000; + scheduleCues(offset); + setIsPlaying(true); + }; + + // HH:MM:SS 形式に変換する補助関数 + 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") + ); + }; + + // ファイルクリア + const handleClearFile = () => { + handleStop(); + setSrtContent(""); + setCues([]); + }; + + return ( +
+

字幕プレイヤー

+
+ + +
+
+ + {playbackMode === "absolute" && ( +
+ +
+ + : + +
+
+ )} +
+
+ + +
+
+ カウントダウン: {effectiveCountdown} 秒 + + +
+ {cues.length > 0 && ( +
+

字幕一覧

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

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

+
+ )} +
+ ); +}; + +/** + * SRT形式の文字列を解析する関数 + * ユーザー提示のサンプルに基づき、改行コードを正規化後、空行で分割して解析 + */ +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, text }); + } + }); + return cues; +}; + +/** + * "HH:MM:SS,mmm" 形式の文字列を秒数に変換する関数 + */ +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; +}; diff --git a/src-ui/app/main_page/plugins_section/PluginsSection.module.scss b/src-ui/app/main_page/plugins_section/PluginsSection.module.scss new file mode 100644 index 00000000..c6a1a6c7 --- /dev/null +++ b/src-ui/app/main_page/plugins_section/PluginsSection.module.scss @@ -0,0 +1,159 @@ +.container { + padding: 2rem; + background: #1e1e1e; + color: #ffffff; + border-radius: 1rem; + max-width: 60rem; + margin: 0 auto; + flex-shrink: 0; + height: 100%; + overflow: auto; + + h1 { + font-size: 2.4rem; + margin-bottom: 1.5rem; + text-align: center; + } + + 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; + } + + input[type="file"] { + padding: 0.5rem; + } + + // ボタンの基本スタイル + 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; + } + } + + // ファイルクリア用ボタン + .file-clear { + background: #6c757d; + color: #fff; + &:hover { + background: #5a6268; + } + } + + // 「再生中」状態(クリック不可)用のスタイル + .is_playing { + background: #6c757d; + cursor: not-allowed; + } + + // カウントダウン表示用エリア + .countdown { + margin-top: 1rem; + font-size: 1.6rem; + display: flex; + align-items: center; + gap: 1rem; + + span { + font-weight: bold; + } + + button { + font-size: 1.6rem; + padding: 0.5rem 1rem; + border: none; + border-radius: 0.4rem; + background: #28a745; + color: #fff; + cursor: pointer; + transition: background 0.3s; + + &:hover { + background: #218838; + } + } + } + + // 再生モードや時刻指定の select 関連(横並びの場合などに調整) + .time-selects { + display: flex; + align-items: center; + gap: 0.5rem; + } + + // 字幕一覧のテーブル + 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; + } +} From 31deeae53fa76445b4bf58bee5f02eac2468db5d Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:32:31 +0900 Subject: [PATCH 02/47] [Update] Support ASS format file. and show actor combine with text. --- .../plugins_section/PluginsSection.jsx | 131 +++++++++++++----- 1 file changed, 96 insertions(+), 35 deletions(-) diff --git a/src-ui/app/main_page/plugins_section/PluginsSection.jsx b/src-ui/app/main_page/plugins_section/PluginsSection.jsx index 7925c4d5..84d966a2 100644 --- a/src-ui/app/main_page/plugins_section/PluginsSection.jsx +++ b/src-ui/app/main_page/plugins_section/PluginsSection.jsx @@ -14,8 +14,8 @@ export const PluginsSection = () => { const [targetHour, setTargetHour] = useState("19"); const [targetMinute, setTargetMinute] = useState("00"); - // カウントダウンの状態 - // initialCountdown: 開始ボタン押下時に計算される元の残り秒数 + // カウントダウン状態 + // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数 const [initialCountdown, setInitialCountdown] = useState(null); // countdownAdjustment: ユーザーが上下ボタンで調整する値(秒単位) const [countdownAdjustment, setCountdownAdjustment] = useState(0); @@ -24,12 +24,12 @@ export const PluginsSection = () => { // cuesScheduled: 字幕タイマーが一度スケジュールされたか const [cuesScheduled, setCuesScheduled] = useState(false); - // setTimeout/setInterval のタイマーID管理用 + // タイマー(setTimeout/setInterval)のID管理用 const timersRef = useRef([]); // ファイル入力リセット用の ref const fileInputRef = useRef(null); - // ファイルアップロード処理 + // ファイルアップロード時の処理 const handleFileUpload = (event) => { const file = event.target.files[0]; if (!file) return; @@ -37,9 +37,15 @@ export const PluginsSection = () => { reader.onload = (e) => { const content = e.target.result; setSrtContent(content); - const parsedCues = parseSRT(content); + let parsedCues = []; + // 拡張子により ASS と SRT を判定 + if (file.name.toLowerCase().endsWith(".ass")) { + parsedCues = parseASS(content); + } else { + parsedCues = parseSRT(content); + } setCues(parsedCues); - console.log("パース結果:", parsedCues); + console.log("Parsed cues:", parsedCues); if (fileInputRef.current) { fileInputRef.current.value = ""; } @@ -49,14 +55,20 @@ export const PluginsSection = () => { // 字幕開始時の処理 const startFunction = (cue) => { - console.log(`字幕開始 (index: ${cue.index}): ${cue.text}`); - sendTextToOverlay(cue.text); + 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(""); }; @@ -75,7 +87,7 @@ export const PluginsSection = () => { setCuesScheduled(false); }; - // cues のスケジュールを行う(offset は countdownAdjustment * 1000) + // cues のスケジュールを行う(字幕開始時のオフセットは countdownAdjustment * 1000) const scheduleCues = (offset) => { cues.forEach((cue) => { const startDelay = cue.startTime * 1000 + offset; @@ -93,7 +105,6 @@ export const PluginsSection = () => { // カウントダウンタイマーの開始 const startCountdownInterval = (initialValue) => { - // 初期表示は (initialValue + countdownAdjustment) setEffectiveCountdown(initialValue + countdownAdjustment); const countdownInterval = setInterval(() => { setEffectiveCountdown((prev) => { @@ -150,7 +161,8 @@ export const PluginsSection = () => { ) { sendTextToOverlay("Start."); console.log("Start."); - scheduleCues(0); + // オフセットは countdownAdjustment × 1000 を字幕に反映 + scheduleCues(countdownAdjustment * 1000); setCuesScheduled(true); } }, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]); @@ -190,11 +202,11 @@ export const PluginsSection = () => {

字幕プレイヤー

{playbackMode === "absolute" && (
- +
{playbackMode === "absolute" && ( -
+
-
+
setTargetMinute(e.target.value)} - className={styles.select} + className={styles.time_selects_item} > {Array.from({ length: 60 }, (_, i) => { const minute = i.toString().padStart(2, "0"); @@ -278,23 +293,55 @@ export const SubtitleSystemContainer = () => { {/* カウントダウン表示:字幕開始前は常に表示 */} {effectiveCountdown !== null && !cuesScheduled && (
- カウントダウン: {effectiveCountdown} 秒 - - + カウントダウン: {secToDayTime(effectiveCountdown)} +
+ {/* 1分単位の調整ボタン */} +
+ + +
+ {/* 1秒単位の調整ボタン */} +
+ + +
+
)} {/* 字幕一覧の表示(relative モードの場合、クリックでジャンプ) */} @@ -408,3 +455,22 @@ const parseTime = (timeString) => { const [hours, minutes, seconds] = hms.split(":").map(Number); return hours * 3600 + minutes * 60 + seconds + Number(ms) / 1000; }; + +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 = `${hour}:${min}:${sec}`; + } else if (min !== 0) { + time = `${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")}`; + } else { + time = `${String(sec).padStart(2, "0")}`; + } + return time; +}; diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss index 515842e7..ca0bc25d 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss +++ b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss @@ -1,157 +1,179 @@ .container { - padding: 4rem; - background: #1e1e1e; - color: #ffffff; - border-radius: 1rem; - flex-shrink: 0; - height: 100%; - overflow: auto; - - h1 { - font-size: 2.4rem; - margin-bottom: 1.5rem; - text-align: center; - } - - 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; - } - - input[type="file"] { - padding: 0.5rem; - } - - // ボタンの基本スタイル - 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; - } - } - - // ファイルクリア用ボタン - .file-clear { - background: #6c757d; - color: #fff; - &:hover { - background: #5a6268; - } - } - - // 「再生中」状態(クリック不可)用のスタイル - .is_playing { - background: #6c757d; - cursor: not-allowed; - } - - // カウントダウン表示用エリア - .countdown { - margin-top: 1rem; - font-size: 1.6rem; + padding: 4rem; + background: #1e1e1e; + color: #ffffff; + border-radius: 1rem; + flex-shrink: 0; + height: 100%; + overflow: auto; display: flex; - align-items: center; - gap: 1rem; + flex-direction: column; + gap: 2rem; - span { - font-weight: bold; + h1 { + font-size: 2.4rem; + margin-bottom: 1.5rem; + text-align: center; } + 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; + } + + input[type="file"] { + padding: 0.5rem; + } + + // ボタンの基本スタイル button { - font-size: 1.6rem; - padding: 0.5rem 1rem; - border: none; - border-radius: 0.4rem; - background: #28a745; - color: #fff; - cursor: pointer; - transition: background 0.3s; + font-size: 1.8rem; + padding: 1rem 2rem; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: background 0.3s; + margin-right: 1rem; - &:hover { - background: #218838; - } + &:focus { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + } } - } - // 再生モードや時刻指定の select 関連(横並びの場合などに調整) - .time-selects { - display: flex; - align-items: center; - gap: 0.5rem; - } + // 再生開始用ボタン(通常時) + .primary { + background: #007bff; + color: #fff; + &:hover { + background: #0056b3; + } + } - // 字幕一覧のテーブル - table { - width: 100%; - border-collapse: collapse; - margin-top: 2rem; + // 再生停止用ボタン + .secondary { + background: #dc3545; + color: #fff; + &:hover { + background: #a71d2a; + } + } + + // ファイルクリア用ボタン + .file_clear { + background: #6c757d; + color: #fff; + &:hover { + background: #5a6268; + } + } + + // 「再生中」状態(クリック不可)用のスタイル + .is_playing { + background: #6c757d; + cursor: not-allowed; + pointer-events: none; + } + + // カウントダウン表示用エリア + .countdown { + margin-top: 1rem; + font-size: 2.6rem; + display: flex; + align-items: center; + gap: 1rem; + flex-direction: column; + + span { + font-weight: bold; + } + + button { + padding: 0.5rem 1rem; + font-size: 1.6rem; + border: none; + border-radius: 0.4rem; + background: #28a745; + color: #fff; + cursor: pointer; + transition: background 0.3s; + + &:hover { + background: #218838; + } + } + } + + // 再生モードや時刻指定の select 関連(横並びの場合などに調整) + .time_selects { + display: flex; + align-items: center; + gap: 0.5rem; + } + + // 字幕一覧のテーブル + 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; + padding: 1rem; + border: 0.1rem solid #444; + text-align: left; + font-size: 1.4rem; } th { - background: #555; + background: #555; } tbody { - tr { + tr { cursor: pointer; transition: background 0.2s; &:nth-child(even) { - background: #2a2a2a; + background: #2a2a2a; } &:hover { - background: #444; + background: #444; + } } - } } - } + } - .subtitle_lists { - font-size: 1.4rem; - } } +.subtitle_lists { + font-size: 1.4rem; +} + +.time_section { + display: flex; + gap: 2rem; + justify-content: center; + align-items: center; +} +.time_selects_item { + gap: 0.4rem; + width: 6rem; + text-align: center; +} + +.adjust_button_wrapper { + display: flex; + gap: 2rem; +} \ No newline at end of file From a9c5ccdbb871d67be7a9177485a80a62e1df82f5 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 15 Feb 2025 08:46:15 +0900 Subject: [PATCH 05/47] [Update/Refactor] Subtitle system v1.1. Organize file system and change the design. --- src-ui/app/App.jsx | 4 + .../SubtitleSystemContainer.jsx | 485 +----------------- .../SubtitleSystemContainer.module.scss | 145 ++---- .../_controllers/SubtitlesController.jsx | 39 ++ .../_logics/useSubtitles.jsx | 193 +++++++ .../_subtitles_utils.js | 112 ++++ .../CountdownContainer.jsx | 72 +++ .../CountdownContainer.module.scss | 47 ++ .../InputFileContainer.jsx | 62 +++ .../InputFileContainer.module.scss | 42 ++ .../ModeSelectorContainer.jsx | 74 +++ .../ModeSelectorContainer.module.scss | 50 ++ .../PlayControlContainer.jsx | 33 ++ .../PlayControlContainer.module.scss | 31 ++ .../SubtitlesListContainer.jsx | 45 ++ .../SubtitlesListContainer.module.scss | 0 src-ui/store.js | 20 +- 17 files changed, 895 insertions(+), 559 deletions(-) create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/_subtitles_utils.js create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.module.scss create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.module.scss create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.module.scss create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx create mode 100644 src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index 86b5430d..03fa2f8c 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -21,6 +21,8 @@ import { SnackbarController } from "./snackbar_controller/SnackbarController"; import styles from "./App.module.scss"; import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common"; +import { SubtitlesController } from "./main_page/main_section/subtitle_system_container/_controllers/subtitlesController.jsx"; + export const App = () => { const { currentIsVrctAvailable } = useIsVrctAvailable(); const { currentIsBackendReady } = useIsBackendReady(); @@ -53,6 +55,8 @@ const Contents = () => { const { currentIsSoftwareUpdating } = useIsSoftwareUpdating(); return ( <> + + {currentIsSoftwareUpdating.data === false ? diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx index 17e1dc82..144aefb0 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx +++ b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx @@ -1,476 +1,45 @@ -import React, { useState, useRef, useEffect } from "react"; import styles from "./SubtitleSystemContainer.module.scss"; -import { useSendTextToOverlay } from "@logics_configs"; +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"; export const SubtitleSystemContainer = () => { - const { sendTextToOverlay } = useSendTextToOverlay(); - const [srtContent, setSrtContent] = useState(""); - const [cues, setCues] = useState([]); - const [isPlaying, setIsPlaying] = useState(false); + // const [srtContent, setSrtContent] = useState(""); + // const [cues, setCues] = useState([]); + // const [isPlaying, setIsPlaying] = useState(false); // 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から) - const [playbackMode, setPlaybackMode] = useState("relative"); + // const [playbackMode, setPlaybackMode] = useState("relative"); // 絶対モード用の再生開始時刻(ドロップダウンで選択、HH:MM) - const [targetHour, setTargetHour] = useState("23"); - const [targetMinute, setTargetMinute] = useState("00"); + // const [targetHour, setTargetHour] = useState("23"); + // const [targetMinute, setTargetMinute] = useState("00"); // カウントダウン状態 - // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数 - const [initialCountdown, setInitialCountdown] = useState(null); + // // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数 + // const [initialCountdown, setInitialCountdown] = useState(null); // countdownAdjustment: ユーザーが上下ボタンで調整する値(秒単位) - const [countdownAdjustment, setCountdownAdjustment] = useState(0); + // const [countdownAdjustment, setCountdownAdjustment] = useState(0); // effectiveCountdown: (initialCountdown + countdownAdjustment) から経過秒数を差し引いた表示用の値 - const [effectiveCountdown, setEffectiveCountdown] = useState(null); + // const [effectiveCountdown, setEffectiveCountdown] = useState(null); // cuesScheduled: 字幕タイマーが一度スケジュールされたか - const [cuesScheduled, setCuesScheduled] = useState(false); + // const [cuesScheduled, setCuesScheduled] = useState(false); - // タイマー(setTimeout/setInterval)のID管理用 - const timersRef = useRef([]); - // カウントダウンタイマー専用の ref - const countdownIntervalRef = useRef(null); - // ファイル入力リセット用の ref - const fileInputRef = useRef(null); - - // ファイルアップロード時の処理 - const handleFileUpload = (event) => { - const file = event.target.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = (e) => { - const content = e.target.result; - setSrtContent(content); - let parsedCues = []; - // 拡張子により ASS と SRT を判定 - if (file.name.toLowerCase().endsWith(".ass")) { - parsedCues = parseASS(content); - } else { - parsedCues = parseSRT(content); - } - setCues(parsedCues); - console.log("Parsed cues:", parsedCues); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }; - reader.readAsText(file); - }; - - // 字幕開始時の処理 - 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(""); - }; - - // すべてのタイマーを停止し、各状態を初期化する - const handleStop = () => { - timersRef.current.forEach((timerId) => { - clearTimeout(timerId); - clearInterval(timerId); - }); - timersRef.current = []; - if (countdownIntervalRef.current) { - clearInterval(countdownIntervalRef.current); - countdownIntervalRef.current = null; - } - console.log("再生を停止しました。"); - setIsPlaying(false); - setInitialCountdown(null); - setEffectiveCountdown(null); - setCountdownAdjustment(0); - setCuesScheduled(false); - }; - - // cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる) - const scheduleCues = (offset) => { - cues.forEach((cue) => { - const startDelay = cue.startTime * 1000 + offset; - const endDelay = cue.endTime * 1000 + offset; - if (startDelay >= 0) { - const timerId = setTimeout(() => startFunction(cue), startDelay); - timersRef.current.push(timerId); - } - if (endDelay >= 0) { - const timerId = setTimeout(() => endFunction(cue), endDelay); - timersRef.current.push(timerId); - } - }); - }; - - // カウントダウンタイマーの開始/再登録(指定した値から1秒ごとに減らす) - const startCountdownInterval = (startValue) => { - // 既存のタイマーがあればクリア - if (countdownIntervalRef.current) { - clearInterval(countdownIntervalRef.current); - } - // 新たな開始値を設定 - setEffectiveCountdown(startValue); - countdownIntervalRef.current = setInterval(() => { - setEffectiveCountdown((prev) => { - if (prev <= 1) { - clearInterval(countdownIntervalRef.current); - return 0; - } - return prev - 1; - }); - }, 1000); - timersRef.current.push(countdownIntervalRef.current); - }; - - // 「再生開始」ボタン押下時の処理 - const handleStart = () => { - handleStop(); - setIsPlaying(true); - setCuesScheduled(false); - - let computedCountdown = 0; - if (playbackMode === "absolute") { - const now = new Date(); - const hour = parseInt(targetHour, 10); - const minute = parseInt(targetMinute, 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 + countdownAdjustment; - startCountdownInterval(startValue); - sendTextToOverlay(startValue.toString()); - }; - - // effectiveCountdown が 0 になったとき、字幕開始 - useEffect(() => { - if ( - isPlaying && - effectiveCountdown !== null && - effectiveCountdown <= 0 && - !cuesScheduled - ) { - sendTextToOverlay("Start."); - console.log("Start."); - // 調整後のタイミングで字幕スケジュールを開始 - scheduleCues(0); - setCuesScheduled(true); - } - - console.log(secToDayTime(effectiveCountdown)); - sendTextToOverlay(secToDayTime(effectiveCountdown)); - }, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]); - - // テーブル内の字幕行をクリック(relative モードのみ)でジャンプ - const handleJump = (jumpCue) => { - if (playbackMode !== "relative") return; - handleStop(); - const offset = -jumpCue.startTime * 1000; - scheduleCues(offset); - setIsPlaying(true); - }; - - // HH:MM:SS 形式に変換する補助関数 - 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") - ); - }; - - // ファイルクリア - const handleClearFile = () => { - handleStop(); - setSrtContent(""); - setCues([]); - }; + // // タイマー(setTimeout/setInterval)のID管理用 + // const timersRef = useRef([]); + // // カウントダウンタイマー専用の ref + // const countdownIntervalRef = useRef(null); return (
-

字幕プレイヤー

-
- - -
-
- - {playbackMode === "absolute" && ( -
- -
- - : - -
-
- )} -
-
- - -
- {/* カウントダウン表示:字幕開始前は常に表示 */} - {effectiveCountdown !== null && !cuesScheduled && ( -
- カウントダウン: {secToDayTime(effectiveCountdown)} -
- {/* 1分単位の調整ボタン */} -
- - -
- {/* 1秒単位の調整ボタン */} -
- - -
-
-
- )} - {/* 字幕一覧の表示(relative モードの場合、クリックでジャンプ) */} - {cues.length > 0 && ( -
-

字幕一覧

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

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

-
- )} +

字幕プレイヤー

+ + + +
+ +
); }; - -/** - * SRT形式の文字列を解析する関数 - * 改行コードを正規化し、空行で分割して解析する - * (actor は存在しないため、空文字列をセット) - */ -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 として抽出する。 - */ -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 秒 - */ -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 時刻文字列を秒数に変換する関数 - */ -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 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 = `${hour}:${min}:${sec}`; - } else if (min !== 0) { - time = `${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")}`; - } else { - time = `${String(sec).padStart(2, "0")}`; - } - return time; -}; diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss index ca0bc25d..ef618213 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss +++ b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss @@ -1,7 +1,6 @@ .container { - padding: 4rem; - background: #1e1e1e; - color: #ffffff; + padding: 2rem 4rem; + background: var(--dark_900_color); border-radius: 1rem; flex-shrink: 0; height: 100%; @@ -9,48 +8,54 @@ display: flex; flex-direction: column; gap: 2rem; +} - h1 { - font-size: 2.4rem; - margin-bottom: 1.5rem; - text-align: center; - } +.title { + font-size: 1.8rem; + text-align: center; + flex-shrink: 0; +} - label { - display: block; - font-size: 1.6rem; - margin-bottom: 0.5rem; - } +.border { + width: 100%; + height: 0.2rem; + background-color: var(--dark_800_color); + flex-shrink: 0; - input, - select { - font-size: 1.6rem; - padding: 0.5rem; - border-radius: 0.5rem; - border: 0.1rem solid #ccc; - background: #333; - color: #fff; - } +} + + // 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; + // } - input[type="file"] { - padding: 0.5rem; - } // ボタンの基本スタイル - button { - font-size: 1.8rem; - padding: 1rem 2rem; - border: none; - border-radius: 0.5rem; - cursor: pointer; - transition: background 0.3s; - margin-right: 1rem; + // 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); - } - } + // &:focus { + // outline: none; + // box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + // } + // } // 再生開始用ボタン(通常時) .primary { @@ -70,14 +75,7 @@ } } - // ファイルクリア用ボタン - .file_clear { - background: #6c757d; - color: #fff; - &:hover { - background: #5a6268; - } - } + // 「再生中」状態(クリック不可)用のスタイル .is_playing { @@ -86,41 +84,6 @@ pointer-events: none; } - // カウントダウン表示用エリア - .countdown { - margin-top: 1rem; - font-size: 2.6rem; - display: flex; - align-items: center; - gap: 1rem; - flex-direction: column; - - span { - font-weight: bold; - } - - button { - padding: 0.5rem 1rem; - font-size: 1.6rem; - border: none; - border-radius: 0.4rem; - background: #28a745; - color: #fff; - cursor: pointer; - transition: background 0.3s; - - &:hover { - background: #218838; - } - } - } - - // 再生モードや時刻指定の select 関連(横並びの場合などに調整) - .time_selects { - display: flex; - align-items: center; - gap: 0.5rem; - } // 字幕一覧のテーブル table { @@ -154,26 +117,8 @@ } } } - } - } + .subtitle_lists { font-size: 1.4rem; } - -.time_section { - display: flex; - gap: 2rem; - justify-content: center; - align-items: center; -} -.time_selects_item { - gap: 0.4rem; - width: 6rem; - text-align: center; -} - -.adjust_button_wrapper { - display: flex; - gap: 2rem; -} \ No newline at end of file diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx new file mode 100644 index 00000000..720c3d28 --- /dev/null +++ b/src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx @@ -0,0 +1,39 @@ +import { useSendTextToOverlay } from "@logics_configs"; +import { useSubtitles } from "../_logics/useSubtitles"; +import { secToDayTime } from "../_subtitles_utils" +import { useEffect } from "react"; +export const SubtitlesController = () => { + 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/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx new file mode 100644 index 00000000..9ce1d626 --- /dev/null +++ b/src-ui/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx @@ -0,0 +1,193 @@ +import { useSendTextToOverlay } from "@logics_configs"; +import { + useStore_SubtitleFileName, + useStore_IsSubtitlePlaying, + useStore_SubtitlePlaybackMode, + useStore_SubtitleAbsoluteTargetTime, + useStore_IsCuesScheduled, + useStore_CountdownAdjustment, + useStore_EffectiveCountdown, + useStore_SubtitleCues, + + useStore_SubtitleTimers, + useStore_SubtitleCountdownTimerId, +} from "@store"; + +export const useSubtitles = () => { + const { sendTextToOverlay } = useSendTextToOverlay(); + const { currentSubtitleFileName, updateSubtitleFileName } = useStore_SubtitleFileName(); + const { currentIsSubtitlePlaying, updateIsSubtitlePlaying } = useStore_IsSubtitlePlaying(); + const { currentSubtitlePlaybackMode, updateSubtitlePlaybackMode } = useStore_SubtitlePlaybackMode(); + const { currentSubtitleAbsoluteTargetTime, updateSubtitleAbsoluteTargetTime } = useStore_SubtitleAbsoluteTargetTime(); + const { currentIsCuesScheduled, updateIsCuesScheduled } = useStore_IsCuesScheduled(); + + const { currentCountdownAdjustment, updateCountdownAdjustment } = useStore_CountdownAdjustment(); + const { currentEffectiveCountdown, updateEffectiveCountdown } = useStore_EffectiveCountdown(); + const { currentSubtitleCues, updateSubtitleCues } = useStore_SubtitleCues(); + + // タイマー(setTimeout/setInterval)のID管理用 + const { currentSubtitleTimers, updateSubtitleTimers, addSubtitleTimers } = useStore_SubtitleTimers(); + // const timersRef = useRef([]); + // カウントダウンタイマー専用の ref + const { currentSubtitleCountdownTimerId, updateSubtitleCountdownTimerId, AddSubtitleCountdownTimerId } = 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/app/main_page/main_section/subtitle_system_container/_subtitles_utils.js b/src-ui/app/main_page/main_section/subtitle_system_container/_subtitles_utils.js new file mode 100644 index 00000000..8bed1f7b --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.jsx new file mode 100644 index 00000000..419db219 --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.module.scss new file mode 100644 index 00000000..0ad5a7ff --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.jsx new file mode 100644 index 00000000..1e234765 --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.module.scss new file mode 100644 index 00000000..91966cbb --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx new file mode 100644 index 00000000..e19ddba2 --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss new file mode 100644 index 00000000..a3c805d9 --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.jsx new file mode 100644 index 00000000..0e4fdbeb --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.module.scss new file mode 100644 index 00000000..ea5de457 --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx new file mode 100644 index 00000000..b997870e --- /dev/null +++ b/src-ui/app/main_page/main_section/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/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/src-ui/store.js b/src-ui/store.js index 17c74d20..dede547b 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -284,4 +284,22 @@ export const { atomInstance: Atom_IsOpenedTranslatorSelector, useHook: useStore_ export const { atomInstance: Atom_SupportersData, useHook: useStore_SupportersData } = createAtomWithHook(null, "SupportersData", {is_state_ok: true}); export const { atomInstance: Atom_VrctPosterIndex, useHook: useStore_VrctPosterIndex } = createAtomWithHook(0, "VrctPosterIndex"); -export const { atomInstance: Atom_PosterShowcaseWorldPageIndex, useHook: useStore_PosterShowcaseWorldPageIndex } = createAtomWithHook(0, "PosterShowcaseWorldPageIndex"); \ No newline at end of file +export const { atomInstance: Atom_PosterShowcaseWorldPageIndex, useHook: useStore_PosterShowcaseWorldPageIndex } = createAtomWithHook(0, "PosterShowcaseWorldPageIndex"); + +// ----------------------------------------------- +// Subtitles +// ----------------------------------------------- +export const { atomInstance: Atom_IsSubtitlePlaying, useHook: useStore_IsSubtitlePlaying } = createAtomWithHook(false, "IsSubtitlePlaying", { is_state_ok: true }); +export const { atomInstance: Atom_SubtitlePlaybackMode, useHook: useStore_SubtitlePlaybackMode } = createAtomWithHook("relative", "SubtitlePlaybackMode", { is_state_ok: true }); +export const { atomInstance: Atom_SubtitleAbsoluteTargetTime, useHook: useStore_SubtitleAbsoluteTargetTime } = createAtomWithHook({ + hour: "23", + minute: "00", +}, "SubtitleAbsoluteTargetTime", { is_state_ok: true }); +export const { atomInstance: Atom_IsCuesScheduled, useHook: useStore_IsCuesScheduled } = createAtomWithHook(false, "IsCuesScheduled", { is_state_ok: true }); +export const { atomInstance: Atom_CountdownAdjustment, useHook: useStore_CountdownAdjustment } = createAtomWithHook(0, "CountdownAdjustment", { is_state_ok: true }); +export const { atomInstance: Atom_EffectiveCountdown, useHook: useStore_EffectiveCountdown } = createAtomWithHook(null, "EffectiveCountdown", { is_state_ok: true }); +export const { atomInstance: Atom_SubtitleCues, useHook: useStore_SubtitleCues } = createAtomWithHook([], "SubtitleCues", { is_state_ok: true }); + +export const { atomInstance: Atom_SubtitleTimers, useHook: useStore_SubtitleTimers } = createAtomWithHook([], "SubtitleTimers", { is_state_ok: true }); +export const { atomInstance: Atom_SubtitleCountdownTimerId, useHook: useStore_SubtitleCountdownTimerId } = createAtomWithHook([], "SubtitleCountdownTimerId", { is_state_ok: true }); +export const { atomInstance: Atom_SubtitleFileName, useHook: useStore_SubtitleFileName } = createAtomWithHook("ファイルが選択されていません", "SubtitleFileName", { is_state_ok: true }); \ No newline at end of file From 22ada89fa68b8ea2cb09951f05b4bc7eaeee8f9c Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:22:22 +0900 Subject: [PATCH 06/47] [TMP] Plugins system. --- package-lock.json | 91 ++++ package.json | 2 + src-tauri/Cargo.lock | 507 +++++++++++++++++- src-tauri/Cargo.toml | 4 +- src-tauri/plugins/plugin_examples/index.jsx | 22 + .../main_container/MainContainer.jsx | 19 + src-tauri/src/main.rs | 21 +- src-tauri/tauri.conf.json | 15 +- src-ui/app/App.jsx | 2 + .../_app_controllers/PluginsController.jsx | 17 + src-ui/app/_app_controllers/index.js | 3 +- .../setting_box/SettingBox.jsx | 3 + .../setting_section/setting_box/index.js | 1 + .../setting_box/plugins/Plugins.jsx | 60 +++ .../setting_box/plugins/Plugins.module.scss | 5 + .../sidebar_section/SidebarSection.jsx | 1 + .../main_page/main_section/MainSection.jsx | 6 +- .../app/main_page/main_section/PluginHost.jsx | 20 + src-ui/logics/configs/index.js | 2 + src-ui/logics/configs/plugins/usePlugins.js | 190 +++++++ src-ui/store.js | 6 +- 21 files changed, 990 insertions(+), 7 deletions(-) create mode 100644 src-tauri/plugins/plugin_examples/index.jsx create mode 100644 src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx create mode 100644 src-ui/app/_app_controllers/PluginsController.jsx create mode 100644 src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx create mode 100644 src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss create mode 100644 src-ui/app/main_page/main_section/PluginHost.jsx create mode 100644 src-ui/logics/configs/plugins/usePlugins.js diff --git a/package-lock.json b/package-lock.json index ebda8372..8e87cde6 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", @@ -20,6 +21,7 @@ "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-i18next": "15.2.0", @@ -271,6 +273,14 @@ "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", @@ -2330,6 +2340,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", @@ -3424,6 +3439,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 +3953,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 +3984,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", @@ -4364,6 +4403,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", @@ -4510,6 +4554,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", @@ -4659,6 +4708,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", @@ -4836,6 +4904,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", @@ -4919,6 +4992,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", @@ -5022,6 +5100,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", @@ -5338,6 +5424,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", diff --git a/package.json b/package.json index 48ee7f7e..eafff304 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", @@ -35,6 +36,7 @@ "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-i18next": "15.2.0", 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..21966762 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-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/plugin_examples/index.jsx b/src-tauri/plugins/plugin_examples/index.jsx new file mode 100644 index 00000000..a2b966b7 --- /dev/null +++ b/src-tauri/plugins/plugin_examples/index.jsx @@ -0,0 +1,22 @@ +import React from "react"; + +import { MainContainer } from "./main_container/MainContainer"; + +export const init = (plugin_context) => { + const { useHook: useStore_CountPluginState } = plugin_context.createAtomWithHook({ count: 6 }, "CountPluginState"); + + const EntryComponents = () => { + + return ( + + ); + + }; + + // UI の"main_section"拡張ポイントにコンポーネントを登録 + plugin_context.registerComponent({ + plugin_id: "dev_vrct_plugin_example_1", + location: "main_section", + component: EntryComponents, + }); +}; \ No newline at end of file diff --git a/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx b/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx new file mode 100644 index 00000000..91dcdf90 --- /dev/null +++ b/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx @@ -0,0 +1,19 @@ + + +export const MainContainer = ({useStore_CountPluginState}) => { + const { updateCountPluginState, currentCountPluginState } = useStore_CountPluginState(); + const incrementCount = () => { + updateCountPluginState((prev_value) => { + return { count: prev_value.data.count + 1 } + }); + }; + + return ( +
+

Dev Plugin Count: {currentCountPluginState?.data?.count}

+ +
+ ); +}; \ 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 9b869f1a..6e6fca0c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,6 +30,18 @@ "globalShortcut": { "all": true }, + "fs": { + "readDir": true, + "readFile": true, + "exists": true, + "writeFile": true, + "createDir": true, + "scope": ["$RESOURCE/**", "**/src-tauri/target/debug/plugins/**"] + }, + "http": { + "request": true, + "scope": ["https://api.github.com/repos/**", "https://github.com/**"] + }, "shell": { "all": false, "open": true, @@ -74,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 03fa2f8c..fe4f5745 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"; @@ -40,6 +41,7 @@ export const App = () => { + {(currentIsBackendReady.data === false || currentIsVrctAvailable.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..6ccc231b --- /dev/null +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import { usePlugins } from "@logics_configs"; + +// ホスト側でReactやjotaiをグローバル変数として提供 +import ReactModule from "react"; +if (typeof window !== "undefined") { + window.React = ReactModule; +} + +export const PluginsController = () => { + const { loadAllPlugins } = usePlugins(); + useEffect(() => { + loadAllPlugins(); + }, []); + + return null; +}; \ 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/config_page/setting_section/setting_box/SettingBox.jsx b/src-ui/app/config_page/setting_section/setting_box/SettingBox.jsx index e61067a7..b149da0d 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/index.js b/src-ui/app/config_page/setting_section/setting_box/index.js index dc5798c3..abb876ae 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/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx new file mode 100644 index 00000000..1954b48d --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from "react"; +import { usePlugins } from "@logics_configs"; +import styles from "./Plugins.module.scss"; + +export const Plugins = () => { + return ( +
+ +
+ ); +}; + +const PluginDownloadContainer = () => { + const [plugin_list, set_plugin_list] = useState([]); + const [download_progress, set_download_progress] = useState({}); + + const { downloadAndExtractPlugin } = usePlugins(); + + useEffect(() => { + // GitHub上のJSONファイルからプラグインリストを取得 + const fetchPluginList = async () => { + try { + const response = await fetch( + "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json" + ); + if (!response.ok) { + throw new Error("Failed to fetch plugin list"); + } + const data = await response.json(); + set_plugin_list(data); + } catch (error) { + console.error("Error fetching plugin list:", error); + } + }; + fetchPluginList(); + }, []); + + const handleDownload = async (plugin) => { + await downloadAndExtractPlugin(plugin); + }; + + return ( +
+ {plugin_list.map((plugin) => ( +
+

{plugin.title}

+ + {download_progress[plugin.plugin_id] !== undefined && ( +
+ Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}% +
+ )} +
+ ))} +
+ ); +}; + diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss new file mode 100644 index 00000000..a49fed11 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss @@ -0,0 +1,5 @@ +.container { + display: flex; + gap: 6.4rem; + flex-direction: column; +} \ No newline at end of file diff --git a/src-ui/app/config_page/sidebar_section/SidebarSection.jsx b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx index 6938f97f..5339de0d 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/main_page/main_section/MainSection.jsx b/src-ui/app/main_page/main_section/MainSection.jsx index a918f1dd..2dec160c 100644 --- a/src-ui/app/main_page/main_section/MainSection.jsx +++ b/src-ui/app/main_page/main_section/MainSection.jsx @@ -9,12 +9,16 @@ import { useStore_IsOpenedLanguageSelector } from "@store"; import { useLanguageSettings } from "@logics_main"; import { useEffect } from "react"; import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer"; + +import { PluginHost } from "./PluginHost"; + export const MainSection = () => { return (
- + {/* */} + {/* */}
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..8fc162ff --- /dev/null +++ b/src-ui/app/main_page/main_section/PluginHost.jsx @@ -0,0 +1,20 @@ +// PluginHost.jsx +import React from "react"; +import { useStore_LoadedPluginsList } from "@store"; + +// export const PluginHost = ({ location }) => { +export const PluginHost = () => { + const { currentLoadedPluginsList } = useStore_LoadedPluginsList(); + console.log(currentLoadedPluginsList.data); + + return ( +
+ {currentLoadedPluginsList.data + .filter((plugin) => plugin.location === "main_section") + .map((plugin, index) => { + const PluginComponent = plugin.component; + return PluginComponent ? : null; + })} +
+ ); +}; \ No newline at end of file diff --git a/src-ui/logics/configs/index.js b/src-ui/logics/configs/index.js index 22439030..85d7c553 100644 --- a/src-ui/logics/configs/index.js +++ b/src-ui/logics/configs/index.js @@ -60,6 +60,8 @@ 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 diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js new file mode 100644 index 00000000..41832944 --- /dev/null +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -0,0 +1,190 @@ + import { invoke } from '@tauri-apps/api/tauri'; + import { createAtomWithHook, useStore_LoadedPluginsList } from "@store"; + import { transform } from "@babel/standalone"; + import { writeFile, createDir, exists, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; + const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); + import JSZip from "jszip"; + import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; + + export const usePlugins = () => { + const { updateLoadedPluginsList } = useStore_LoadedPluginsList(); + + const plugin_context = { + registerComponent: ({ plugin_id, location, component }) => { + if (!plugin_id || !location || !component) { + return console.error("An invalid plugin was detected.", plugin_id, location, component); + } + updateLoadedPluginsList((prev) => { + const filtered = prev.data.filter(item => item.plugin_id !== plugin_id); + return [...filtered, { plugin_id, location, component }]; + }); + }, + createAtomWithHook: (...args) => createAtomWithHook(...args) + }; + + const asyncLoadPlugin = async (plugin_relative_path) => { + plugin_relative_path = "plugins/" + plugin_relative_path; + console.log("plugin_relative_path",plugin_relative_path); + + try { + const plugin_code = await readTextFile(plugin_relative_path, { dir: BaseDirectory.Resource, recursive: true }); + const cleanedCode = removeImportStatements(plugin_code); + const transpiled_code = transform(cleanedCode, { + 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(plugin_context); + } + } catch (error) { + console.error("Failed to load plugin from", plugin_relative_path, error); + } + }; + + const loadAllPlugins = async () => { + if (import.meta.env.DEV) { + // ホットリロード対応 src-tauri以下にあるpluginsディレクトリから直接読み込み(開発用) + Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => { + console.log(plugin_module); + if (plugin_module && plugin_module.init) { + plugin_module.init(plugin_context); + } + }); + } else { + try { + const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true }); + for (const target_dir of plugin_files) { + console.log(target_dir); + + const target_path = target_dir.name + "\\index.jsx"; + await asyncLoadPlugin(target_path, plugin_context); + } + } catch (error) { + console.error("Error loading plugins:", error); + } + } + }; + + const downloadAndExtractPlugin = async (plugin) => { + try { + const plugin_zip_url = await fetchLatestPluginZipUrl(plugin); + console.log("Latest plugin zip URL:", plugin_zip_url); + + // Rust コマンド経由で zip をダウンロード + const base64Zip = await invoke("download_zip_asset", { url: plugin_zip_url }); + // base64Zip は文字列なので、デコードして Uint8Array に変換 + const binaryString = atob(base64Zip); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // JSZip で zip を解凍 + const zip = await JSZip.loadAsync(bytes); + + // const plugin_dir_exists = await exists("plugins", { dir: BaseDirectory.Resource, recursive: true }); + await createDir("plugins/" + plugin.asset_name.replace(".zip", ""), { dir: BaseDirectory.Resource, recursive: true }); + // if (!plugin_dir_exists) { + // } + const target_plugin_path = "plugins/" + plugin.asset_name.replace(".zip", ""); + + + const filePromises = []; + zip.forEach((relativePath, zipEntry) => { + // .git 以下のファイルはスキップ + if (relativePath.startsWith(".git") || relativePath.includes("/.git/")) { + // console.log("Skipping .git file: " + relativePath); + return; + } + + + const filePath = target_plugin_path + "/" + relativePath; + + if (zipEntry.dir) { + // フォルダの場合、ディレクトリを作成 + filePromises.push( + createDir(filePath, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => { + console.log(err); + + if (!err.message?.includes("already exists")) { + console.error("Failed to create directory:", filePath, err); + } + }) + ); + } else { + // ファイルの場合、ディレクトリを作成してから書き込む + const dirPath = filePath.substring(0, filePath.lastIndexOf("/")); // 親ディレクトリのパス + + const promise = createDir(dirPath, { dir: BaseDirectory.Resource, recursive: true }) + .catch((err) => { + if (!err.message?.includes("already exists")) { + console.error("Failed to create parent directory:", dirPath, err); + } + }) + .then(() => zipEntry.async("text")) + .then(async (fileData) => { + await writeFile(filePath, fileData, { dir: BaseDirectory.Resource, recursive: true }); + }); + + filePromises.push(promise); + } + }); + + await Promise.all(filePromises); + console.log("Plugin downloaded successfully."); + + const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/" + "index.jsx" + console.log("index_file_relative_path", index_file_relative_path); + + await asyncLoadPlugin(index_file_relative_path); + + console.log("Plugin loaded successfully."); + } catch (error) { + console.error("Error downloading and extracting plugin:", error); + } + }; + + // JSON内のURLから GitHub API を使って最新リリース情報を取得し、 + // assets 配列から plugin.asset_name に一致するアセットの browser_download_url を返す + const fetchLatestPluginZipUrl = async (plugin) => { + const api_url = plugin.url; + const response = await tauriFetch(api_url, { + method: "GET", + responseType: ResponseType.Json, + headers: { + "Accept": "application/vnd.github+json", + "User-Agent": "VRCTPluginApp" + } + }); + 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; + }; + + return { + loadAllPlugins, + downloadAndExtractPlugin, + }; +}; + +const removeImportStatements = (code) => { + return code + .split("\n") + .filter(line => !line.match(/^import\s+.*['"]react['"]/)) + .join("\n"); +}; diff --git a/src-ui/store.js b/src-ui/store.js index 9a2967c4..da4c1f45 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -34,7 +34,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", @@ -274,6 +274,10 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA toggle_transcription_receive: null, }, "Hotkeys"); +// Plugins +export const { atomInstance: Atom_InstalledPluginsPath, useHook: useStore_InstalledPluginsPath } = createAtomWithHook([], "InstalledPluginsPath"); +export const { atomInstance: Atom_LoadedPluginsList, useHook: useStore_LoadedPluginsList } = createAtomWithHook([], "LoadedPluginsList"); + // 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"); From 48c6e7d69f5d54c79336166b14556b79180464b8 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 8 Mar 2025 18:43:56 +0900 Subject: [PATCH 07/47] [TMP 2] Plugins system. support directory structure and store system.(the plugin needs build at each project) --- src-tauri/plugins/plugin_examples/index.jsx | 17 +++---- .../main_container/MainContainer.jsx | 17 ++++--- .../plugins/plugin_examples/store/store.js | 22 ++++++++++ .../setting_box/plugins/Plugins.jsx | 1 + src-ui/logics/configs/plugins/usePlugins.js | 44 +++++++++---------- 5 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 src-tauri/plugins/plugin_examples/store/store.js diff --git a/src-tauri/plugins/plugin_examples/index.jsx b/src-tauri/plugins/plugin_examples/index.jsx index a2b966b7..80f3befa 100644 --- a/src-tauri/plugins/plugin_examples/index.jsx +++ b/src-tauri/plugins/plugin_examples/index.jsx @@ -1,22 +1,19 @@ import React from "react"; - +import { initStore } from "./store/store"; import { MainContainer } from "./main_container/MainContainer"; export const init = (plugin_context) => { - const { useHook: useStore_CountPluginState } = plugin_context.createAtomWithHook({ count: 6 }, "CountPluginState"); + initStore(plugin_context.createAtomWithHook); const EntryComponents = () => { - - return ( - - ); - + return ; }; - // UI の"main_section"拡張ポイントにコンポーネントを登録 plugin_context.registerComponent({ - plugin_id: "dev_vrct_plugin_example_1", + plugin_id: "plugin_example_1_my_plugin", location: "main_section", component: EntryComponents, }); -}; \ No newline at end of file +}; + +export default init; \ No newline at end of file diff --git a/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx b/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx index 91dcdf90..3a56e747 100644 --- a/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx +++ b/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx @@ -1,19 +1,18 @@ +import { useStore } from "../store/store"; +export const MainContainer = () => { + const { updateCountPluginState, currentCountPluginState } = useStore("useStore_CountPluginState"); -export const MainContainer = ({useStore_CountPluginState}) => { - const { updateCountPluginState, currentCountPluginState } = useStore_CountPluginState(); const incrementCount = () => { - updateCountPluginState((prev_value) => { - return { count: prev_value.data.count + 1 } - }); + updateCountPluginState((prev_value) => ({ + count: prev_value.data.count + 1, + })); }; return (
-

Dev Plugin Count: {currentCountPluginState?.data?.count}

- +

1 Zipped Dev Plugin Count: {currentCountPluginState?.data?.count}

+
); }; \ No newline at end of file diff --git a/src-tauri/plugins/plugin_examples/store/store.js b/src-tauri/plugins/plugin_examples/store/store.js new file mode 100644 index 00000000..9efd9dcf --- /dev/null +++ b/src-tauri/plugins/plugin_examples/store/store.js @@ -0,0 +1,22 @@ +const store_hooks = {}; + +export const initStore = (createAtomWithHook) => { + Object.assign(store_hooks, { + useStore_CountPluginState: createAtomWithHook( + { count: 10 }, + "CountPluginState" + ).useHook, + + useStore_AnotherState: createAtomWithHook( + { value: "initial" }, + "AnotherState" + ).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](); +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 1954b48d..954e4b33 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -44,6 +44,7 @@ const PluginDownloadContainer = () => { {plugin_list.map((plugin) => (

{plugin.title}

+

{plugin.plugin_id}

diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 41832944..4120cd5d 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,12 +1,21 @@ - import { invoke } from '@tauri-apps/api/tauri'; - import { createAtomWithHook, useStore_LoadedPluginsList } from "@store"; - import { transform } from "@babel/standalone"; - import { writeFile, createDir, exists, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; - const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); - import JSZip from "jszip"; - import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; +import { invoke } from '@tauri-apps/api/tauri'; +import { createAtomWithHook, useStore_LoadedPluginsList } from "@store"; +import { transform } from "@babel/standalone"; +import { writeFile, createDir, exists, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; +const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); +import JSZip from "jszip"; +import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; - export const usePlugins = () => { +import React from "react"; +// import ReactDOM from "react-dom/client"; + +// グローバルに公開 +window.React = React; +// window.ReactDOM = ReactDOM; +window.React$1 = React; +window.we = React; + +export const usePlugins = () => { const { updateLoadedPluginsList } = useStore_LoadedPluginsList(); const plugin_context = { @@ -24,7 +33,6 @@ const asyncLoadPlugin = async (plugin_relative_path) => { plugin_relative_path = "plugins/" + plugin_relative_path; - console.log("plugin_relative_path",plugin_relative_path); try { const plugin_code = await readTextFile(plugin_relative_path, { dir: BaseDirectory.Resource, recursive: true }); @@ -32,10 +40,11 @@ const transpiled_code = transform(cleanedCode, { presets: [ ["env", { modules: false }], - "react" + "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); @@ -53,7 +62,6 @@ if (import.meta.env.DEV) { // ホットリロード対応 src-tauri以下にあるpluginsディレクトリから直接読み込み(開発用) Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => { - console.log(plugin_module); if (plugin_module && plugin_module.init) { plugin_module.init(plugin_context); } @@ -62,9 +70,7 @@ try { const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true }); for (const target_dir of plugin_files) { - console.log(target_dir); - - const target_path = target_dir.name + "\\index.jsx"; + const target_path = target_dir.name + "/index.es.js"; await asyncLoadPlugin(target_path, plugin_context); } } catch (error) { @@ -76,8 +82,6 @@ const downloadAndExtractPlugin = async (plugin) => { try { const plugin_zip_url = await fetchLatestPluginZipUrl(plugin); - console.log("Latest plugin zip URL:", plugin_zip_url); - // Rust コマンド経由で zip をダウンロード const base64Zip = await invoke("download_zip_asset", { url: plugin_zip_url }); // base64Zip は文字列なので、デコードして Uint8Array に変換 @@ -102,19 +106,15 @@ zip.forEach((relativePath, zipEntry) => { // .git 以下のファイルはスキップ if (relativePath.startsWith(".git") || relativePath.includes("/.git/")) { - // console.log("Skipping .git file: " + relativePath); return; } - const filePath = target_plugin_path + "/" + relativePath; if (zipEntry.dir) { // フォルダの場合、ディレクトリを作成 filePromises.push( createDir(filePath, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => { - console.log(err); - if (!err.message?.includes("already exists")) { console.error("Failed to create directory:", filePath, err); } @@ -142,9 +142,7 @@ await Promise.all(filePromises); console.log("Plugin downloaded successfully."); - const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/" + "index.jsx" - console.log("index_file_relative_path", index_file_relative_path); - + const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/" + "index.es.js" await asyncLoadPlugin(index_file_relative_path); console.log("Plugin loaded successfully."); From 77795192a0eba99f1b63f273914cdb2c8475fdb5 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 11 Mar 2025 01:03:18 +0900 Subject: [PATCH 08/47] [Update] Change download and load plugins structure.(change the plugins' build method vite to webpack. load esm.js ) --- package-lock.json | 425 ++++++++++-------- package.json | 9 +- src-tauri/Cargo.toml | 2 +- src-tauri/plugins/plugin_examples/index.jsx | 1 - .../main_container/MainContainer.jsx | 4 + src-tauri/tauri.conf.json | 3 +- .../_app_controllers/PluginsController.jsx | 7 +- .../setting_box/plugins/Plugins.jsx | 143 +++++- src-ui/logics/configs/plugins/usePlugins.js | 107 ++--- vite.config.js | 5 + 10 files changed, 428 insertions(+), 278 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e87cde6..6cb00da1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,13 +25,14 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "15.2.0", - "react-resizable-layout": "0.7.2" + "react-resizable-layout": "0.7.2", + "semver": "^7.7.1" }, "devDependencies": { "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.0.3", + "vite": "^6.2.1", "vite-plugin-svgr": "4.3.0" } }, @@ -457,9 +458,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" ], @@ -472,9 +473,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" ], @@ -487,9 +488,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" ], @@ -502,9 +503,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" ], @@ -517,9 +518,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" ], @@ -532,9 +533,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" ], @@ -547,9 +548,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" ], @@ -562,9 +563,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" ], @@ -577,9 +578,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" ], @@ -592,9 +593,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" ], @@ -607,9 +608,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" ], @@ -622,9 +623,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" ], @@ -637,9 +638,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" ], @@ -652,9 +653,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" ], @@ -667,9 +668,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" ], @@ -682,9 +683,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" ], @@ -697,9 +698,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" ], @@ -711,10 +712,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" ], @@ -727,9 +743,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" ], @@ -742,9 +758,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" ], @@ -757,9 +773,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" ], @@ -772,9 +788,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" ], @@ -787,9 +803,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" ], @@ -802,9 +818,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" ], @@ -1218,9 +1234,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" ], @@ -1230,9 +1246,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" ], @@ -1242,9 +1258,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" ], @@ -1254,9 +1270,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" ], @@ -1265,10 +1281,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" ], @@ -1278,9 +1318,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" ], @@ -1290,9 +1330,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" ], @@ -1302,9 +1342,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" ], @@ -1313,10 +1353,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" ], @@ -1326,9 +1378,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" ], @@ -1338,9 +1390,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" ], @@ -1350,9 +1402,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" ], @@ -1362,9 +1414,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" ], @@ -1374,9 +1426,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" ], @@ -1386,9 +1438,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" ], @@ -1398,9 +1450,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" ], @@ -2704,9 +2756,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" @@ -2715,30 +2767,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": { @@ -4106,9 +4159,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", @@ -4520,9 +4573,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", @@ -4538,7 +4591,7 @@ } ], "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4832,9 +4885,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" }, @@ -4846,22 +4899,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" } }, @@ -4951,10 +5007,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" }, @@ -5440,13 +5495,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.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", + "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", "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 eafff304..9de73950 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "release-all": "npm run release && npm run release-cuda" }, "dependencies": { - "@babel/standalone": "^7.26.9", + "@babel/standalone": "7.26.9", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@mui/material": "6.2.0", @@ -36,17 +36,18 @@ "jotai": "2.10.3", "js-base64": "3.7.7", "js-yaml": "4.1.0", - "jszip": "^3.10.1", + "jszip": "3.10.1", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "15.2.0", - "react-resizable-layout": "0.7.2" + "react-resizable-layout": "0.7.2", + "semver": "7.7.1" }, "devDependencies": { "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.0.3", + "vite": "6.2.1", "vite-plugin-svgr": "4.3.0" } } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 21966762..cf7e0ada 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" tauri-build = { version = "1", features = [] } [dependencies] -tauri = { version = "1", features = [ "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"] } +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" diff --git a/src-tauri/plugins/plugin_examples/index.jsx b/src-tauri/plugins/plugin_examples/index.jsx index 80f3befa..58441bd8 100644 --- a/src-tauri/plugins/plugin_examples/index.jsx +++ b/src-tauri/plugins/plugin_examples/index.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { initStore } from "./store/store"; import { MainContainer } from "./main_container/MainContainer"; diff --git a/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx b/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx index 3a56e747..eaccc0aa 100644 --- a/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx +++ b/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx @@ -1,4 +1,5 @@ import { useStore } from "../store/store"; +import { useEffect } from "react"; export const MainContainer = () => { const { updateCountPluginState, currentCountPluginState } = useStore("useStore_CountPluginState"); @@ -9,6 +10,9 @@ export const MainContainer = () => { })); }; + useEffect(() => { + }, []) + return (

1 Zipped Dev Plugin Count: {currentCountPluginState?.data?.count}

diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6e6fca0c..b36e8ce4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -36,11 +36,12 @@ "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/**"] + "scope": ["https://api.github.com/repos/**", "https://github.com/**", "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"] }, "shell": { "all": false, diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 6ccc231b..82abb5d2 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -1,10 +1,9 @@ -import { useEffect } from "react"; import { usePlugins } from "@logics_configs"; -// ホスト側でReactやjotaiをグローバル変数として提供 -import ReactModule from "react"; + +import React, { useEffect } from "react"; if (typeof window !== "undefined") { - window.React = ReactModule; + window.React = React; } export const PluginsController = () => { diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 954e4b33..e290f292 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -1,6 +1,12 @@ import React, { useState, useEffect } from "react"; +import semver from "semver"; import { usePlugins } from "@logics_configs"; import styles from "./Plugins.module.scss"; +import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; + +const MAIN_VRCT_VERSION = "3.0.5"; +// PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL +const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"; export const Plugins = () => { return ( @@ -13,26 +19,45 @@ export const Plugins = () => { const PluginDownloadContainer = () => { const [plugin_list, set_plugin_list] = useState([]); const [download_progress, set_download_progress] = useState({}); - const { downloadAndExtractPlugin } = usePlugins(); useEffect(() => { - // GitHub上のJSONファイルからプラグインリストを取得 - const fetchPluginList = async () => { + async function asyncFetchPluginInfoList() { try { - const response = await fetch( - "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json" - ); - if (!response.ok) { - throw new Error("Failed to fetch plugin list"); + // tauriFetch を使用して vrct_plugins_list.json を取得(CORS 対策) + const response = await tauriFetch(PLUGIN_LIST_URL, { + method: "GET", + responseType: ResponseType.Json, + headers: { "Cache-Control": "no-cache" } + }); + if (response.status !== 200) { + throw new Error("Failed to fetch plugin list, status: " + response.status); } - const data = await response.json(); - set_plugin_list(data); + // 取得される plugin_list.json は各プラグインの plugin_info.json への raw URL の配列とする + const plugins_data = response.data; + const updated_list = await Promise.all( + plugins_data.map(async (plugin_data) => { + try { + const plugin_manifest = await asyncFetchPluginManifest(plugin_data.url); + return { ...plugin_manifest }; + } catch (error) { + console.error("Error fetching manifest for URL:", plugin_data.url, error); + // エラー発生時は、plugin_data.title とエラーメッセージを返す + return { + title: plugin_data.title, + plugin_id: plugin_data.plugin_id || plugin_data.title, + error: error.message, + url: plugin_data.url + }; + } + }) + ); + set_plugin_list(updated_list); } catch (error) { - console.error("Error fetching plugin list:", error); + console.error("Error fetching plugin info list:", error); } - }; - fetchPluginList(); + } + asyncFetchPluginInfoList(); }, []); const handleDownload = async (plugin) => { @@ -44,14 +69,24 @@ const PluginDownloadContainer = () => { {plugin_list.map((plugin) => (

{plugin.title}

-

{plugin.plugin_id}

- - {download_progress[plugin.plugin_id] !== undefined && ( -
- Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}% -
+

{plugin.plugin_id}

+ {plugin.error ? ( +

Error: {plugin.error}

+ ) : ( + <> +

Version: {plugin.plugin_version}

+

+ Compatible: {plugin.min_compatible_version} ~ {plugin.max_compatible_version} +

+ + {download_progress[plugin.plugin_id] !== undefined && ( +
+ Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}% +
+ )} + )}
))} @@ -59,3 +94,69 @@ const PluginDownloadContainer = () => { ); }; +// GitHub Releases の latest 情報から plugin_info.json を取得する(tauriFetch を使用) +async function asyncFetchPluginManifest(manifest_url) { + // リリース情報を取得 + const release_response = await tauriFetch(manifest_url, { + method: "GET", + responseType: ResponseType.Json, + headers: { + "Accept": "application/vnd.github+json", + "User-Agent": "VRCTPluginApp" + } + }); + if (release_response.status !== 200) { + throw new Error(`Failed to fetch release info from ${manifest_url}`); + } + const release_data = release_response.data; + // assets 内に plugin_info.json があるかチェック + const manifest_asset = release_data.assets.find(asset => asset.name === "plugin_info.json"); + if (!manifest_asset) { + throw new Error("plugin_info.json not found in release assets"); + } + // plugin_info.json の内容を取得 + const manifest_response = await tauriFetch(manifest_asset.browser_download_url, { + method: "GET", + responseType: ResponseType.Json, + headers: { + "Accept": "application/json", + "User-Agent": "VRCTPluginApp", + "Cache-Control": "no-cache" + } + }); + if (manifest_response.status !== 200) { + throw new Error(`Failed to fetch plugin_info.json from ${manifest_asset.browser_download_url}`); + } + const plugin_manifest = manifest_response.data; + return { + title: plugin_manifest.title, + plugin_id: plugin_manifest.plugin_id, + plugin_version: plugin_manifest.plugin_version, + min_compatible_version: plugin_manifest.min_compatible_version, + max_compatible_version: plugin_manifest.max_compatible_version, + asset_name: plugin_manifest.asset_name, + url: manifest_url + }; +} + +export { PluginDownloadContainer }; + + + + // // プラグインのマニフェスト(plugin.json から取得した情報の例) + // const plugin_manifest = { + // compatible_lower_version: "3.0.4", + // compatible_upper_version: "3.0.6", + // // 他の情報... + // }; + + // 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); + // }; + + // if (isPluginCompatible(currentSoftwareVersion.data, plugin_manifest.compatible_lower_version, plugin_manifest.compatible_upper_version)) { + // console.log("プラグインは互換性があります。"); + // } else { + // console.error("プラグインは現在の VRCT バージョンと互換性がありません。"); + // } \ No newline at end of file diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 4120cd5d..b9ebc97c 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,50 +1,41 @@ import { invoke } from '@tauri-apps/api/tauri'; import { createAtomWithHook, useStore_LoadedPluginsList } from "@store"; +import { useSoftwareVersion } from "@logics_configs"; import { transform } from "@babel/standalone"; -import { writeFile, createDir, exists, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; +import { writeFile, createDir, exists, removeDir, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); import JSZip from "jszip"; import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; -import React from "react"; -// import ReactDOM from "react-dom/client"; - -// グローバルに公開 -window.React = React; -// window.ReactDOM = ReactDOM; -window.React$1 = React; -window.we = React; - export const usePlugins = () => { const { updateLoadedPluginsList } = useStore_LoadedPluginsList(); + const { currentSoftwareVersion } = useSoftwareVersion(); const plugin_context = { registerComponent: ({ plugin_id, location, component }) => { - if (!plugin_id || !location || !component) { - return console.error("An invalid plugin was detected.", plugin_id, location, component); - } - updateLoadedPluginsList((prev) => { - const filtered = prev.data.filter(item => item.plugin_id !== plugin_id); - return [...filtered, { plugin_id, location, component }]; - }); + if (!plugin_id || !location || !component) { + return console.error("An invalid plugin was detected.", plugin_id, location, component); + } + updateLoadedPluginsList((prev) => { + const filtered = prev.data.filter(item => item.plugin_id !== plugin_id); + return [...filtered, { plugin_id, location, component }]; + }); }, createAtomWithHook: (...args) => createAtomWithHook(...args) }; const asyncLoadPlugin = async (plugin_relative_path) => { plugin_relative_path = "plugins/" + plugin_relative_path; - try { const plugin_code = await readTextFile(plugin_relative_path, { dir: BaseDirectory.Resource, recursive: true }); - const cleanedCode = removeImportStatements(plugin_code); - const transpiled_code = transform(cleanedCode, { + 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); @@ -60,7 +51,7 @@ export const usePlugins = () => { const loadAllPlugins = async () => { if (import.meta.env.DEV) { - // ホットリロード対応 src-tauri以下にあるpluginsディレクトリから直接読み込み(開発用) + // 開発時: ホットリロード対応、src-tauri以下のpluginsから直接読み込み Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => { if (plugin_module && plugin_module.init) { plugin_module.init(plugin_context); @@ -70,7 +61,7 @@ export const usePlugins = () => { try { const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true }); for (const target_dir of plugin_files) { - const target_path = target_dir.name + "/index.es.js"; + const target_path = target_dir.name + "/index.esm.js"; await asyncLoadPlugin(target_path, plugin_context); } } catch (error) { @@ -82,68 +73,63 @@ export const usePlugins = () => { const downloadAndExtractPlugin = async (plugin) => { try { const plugin_zip_url = await fetchLatestPluginZipUrl(plugin); - // Rust コマンド経由で zip をダウンロード - const base64Zip = await invoke("download_zip_asset", { url: plugin_zip_url }); - // base64Zip は文字列なので、デコードして Uint8Array に変換 - const binaryString = atob(base64Zip); - const len = binaryString.length; + // 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] = binaryString.charCodeAt(i); + bytes[i] = binary_string.charCodeAt(i); } - // JSZip で zip を解凍 + // JSZip で ZIP を解凍 const zip = await JSZip.loadAsync(bytes); - // const plugin_dir_exists = await exists("plugins", { dir: BaseDirectory.Resource, recursive: true }); - await createDir("plugins/" + plugin.asset_name.replace(".zip", ""), { dir: BaseDirectory.Resource, recursive: true }); - // if (!plugin_dir_exists) { - // } + // 展開先ディレクトリのパス(例:"plugins/" とする) const target_plugin_path = "plugins/" + plugin.asset_name.replace(".zip", ""); + // 既に存在する場合は削除してから新規作成 + 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 filePromises = []; - zip.forEach((relativePath, zipEntry) => { + const file_promises = []; + zip.forEach((relative_path, zip_entry) => { // .git 以下のファイルはスキップ - if (relativePath.startsWith(".git") || relativePath.includes("/.git/")) { + if (relative_path.startsWith(".git") || relative_path.includes("/.git/")) { return; } - - const filePath = target_plugin_path + "/" + relativePath; - - if (zipEntry.dir) { - // フォルダの場合、ディレクトリを作成 - filePromises.push( - createDir(filePath, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => { + 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:", filePath, err); + console.error("Failed to create directory:", file_path, err); } }) ); } else { - // ファイルの場合、ディレクトリを作成してから書き込む - const dirPath = filePath.substring(0, filePath.lastIndexOf("/")); // 親ディレクトリのパス - - const promise = createDir(dirPath, { dir: BaseDirectory.Resource, recursive: true }) + 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:", dirPath, err); + console.error("Failed to create parent directory:", dir_path, err); } }) - .then(() => zipEntry.async("text")) - .then(async (fileData) => { - await writeFile(filePath, fileData, { dir: BaseDirectory.Resource, recursive: true }); + .then(() => zip_entry.async("text")) + .then(async (file_data) => { + await writeFile(file_path, file_data, { dir: BaseDirectory.Resource, recursive: true }); }); - - filePromises.push(promise); + file_promises.push(promise); } }); - await Promise.all(filePromises); + await Promise.all(file_promises); console.log("Plugin downloaded successfully."); - const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/" + "index.es.js" - await asyncLoadPlugin(index_file_relative_path); + const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/index.esm.js"; + await asyncLoadPlugin(index_file_relative_path, plugin_context); console.log("Plugin loaded successfully."); } catch (error) { @@ -151,8 +137,7 @@ export const usePlugins = () => { } }; - // JSON内のURLから GitHub API を使って最新リリース情報を取得し、 - // assets 配列から plugin.asset_name に一致するアセットの browser_download_url を返す + // GitHub API を使用して、最新リリース情報から asset_name に一致するアセットのブラウザダウンロード URL を返す const fetchLatestPluginZipUrl = async (plugin) => { const api_url = plugin.url; const response = await tauriFetch(api_url, { diff --git a/vite.config.js b/vite.config.js index 0d44e3e2..1abffdf1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -33,6 +33,11 @@ export default defineConfig(async () => ({ resolve: { alias: { + "react": path.resolve(__dirname, "node_modules/react"), + "react-dom": path.resolve(__dirname, "node_modules/react-dom"), + + + "@root": path.resolve(__dirname), "@test_data": path.resolve(__dirname, "./test_data.js"), From b0f5751e1129f23260e6e6353e9770847b4ba9fc Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 13 Mar 2025 12:17:36 +0900 Subject: [PATCH 09/47] [Update/Refactor] Fetch and show the plugins info list. Refactor some functions. Try to fetch functions from github api just once when vrct started.(It used to every time plugin tab has opened so easy to reach to the api limit) --- package-lock.json | 8 +- src-ui/app/App.jsx | 2 +- .../_app_controllers/PluginsController.jsx | 17 +- .../_download_button/_DownloadButton.jsx | 41 ++++ .../_DownloadButton.module.scss | 30 +++ .../download_models/DownloadModels.jsx | 46 +--- .../DownloadModels.module.scss | 30 --- .../download_plugins/DownloadPlugins.jsx | 33 +++ .../DownloadPlugins.module.scss | 0 .../setting_box/_components/index.js | 3 +- .../setting_box/plugins/Plugins.jsx | 202 ++++++------------ .../setting_box/plugins/Plugins.module.scss | 30 +++ .../app/main_page/main_section/PluginHost.jsx | 6 +- src-ui/logics/common/index.js | 3 +- src-ui/logics/common/useFetch.js | 21 ++ src-ui/logics/configs/plugins/usePlugins.js | 113 ++++++++-- src-ui/store.js | 2 +- 17 files changed, 350 insertions(+), 237 deletions(-) create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.jsx create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.module.scss create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss create mode 100644 src-ui/logics/common/useFetch.js diff --git a/package-lock.json b/package-lock.json index 6cb00da1..62bd3e7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "tauri-app", "version": "0.0.0", "dependencies": { - "@babel/standalone": "^7.26.9", + "@babel/standalone": "7.26.9", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.0", "@mui/material": "6.2.0", @@ -21,18 +21,18 @@ "jotai": "2.10.3", "js-base64": "3.7.7", "js-yaml": "4.1.0", - "jszip": "^3.10.1", + "jszip": "3.10.1", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "15.2.0", "react-resizable-layout": "0.7.2", - "semver": "^7.7.1" + "semver": "7.7.1" }, "devDependencies": { "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "^6.2.1", + "vite": "6.2.1", "vite-plugin-svgr": "4.3.0" } }, diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index fe4f5745..22c5107c 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -41,7 +41,6 @@ export const App = () => { - {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) ? @@ -57,6 +56,7 @@ const Contents = () => { const { currentIsSoftwareUpdating } = useIsSoftwareUpdating(); return ( <> + diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 82abb5d2..16305224 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -1,15 +1,24 @@ +import React, { useEffect, useRef } from "react"; import { usePlugins } from "@logics_configs"; - -import React, { useEffect } from "react"; if (typeof window !== "undefined") { window.React = React; } export const PluginsController = () => { - const { loadAllPlugins } = usePlugins(); + const hasRunRef = useRef(false); + const { + loadAllPlugins, + asyncUpdatePluginInfoList, + } = usePlugins(); + useEffect(() => { - loadAllPlugins(); + if (!hasRunRef.current) { + asyncUpdatePluginInfoList().then(() => { + loadAllPlugins(); + }); + } + return () => hasRunRef.current = true; }, []); return null; 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..41c3b7d9 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.jsx @@ -0,0 +1,41 @@ +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 ( + + ); + 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..59fabfe3 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_download_button/_DownloadButton.module.scss @@ -0,0 +1,30 @@ +@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/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/download_plugins/DownloadPlugins.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx new file mode 100644 index 00000000..7c1a17b9 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx @@ -0,0 +1,33 @@ +import { + SwitchBox, +} from "../index"; +import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; + +export const DownloadPlugins = ({plugin_info, ...props}) => { + + const option = { + is_pending: plugin_info.is_pending, + is_downloaded: plugin_info.is_downloaded, + } + console.log(plugin_info); + + + return ( +
+ {/* */} + {plugin_info.is_plugin_supported ? + <_DownloadButton + option={option} + downloadStartFunction={props.downloadStartFunction} + /> + : +
+ Unavailable +
+ } +
+ ); +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss new file mode 100644 index 00000000..e69de29b 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 3a295d3c..33b0837d 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 @@ -11,4 +11,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 { DownloadPlugins } from "./download_plugins/DownloadPlugins"; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index e290f292..8201cdd8 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -1,12 +1,7 @@ import React, { useState, useEffect } from "react"; -import semver from "semver"; import { usePlugins } from "@logics_configs"; import styles from "./Plugins.module.scss"; -import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; - -const MAIN_VRCT_VERSION = "3.0.5"; -// PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL -const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"; +import { DownloadPlugins } from "../_components"; export const Plugins = () => { return ( @@ -17,146 +12,85 @@ export const Plugins = () => { }; const PluginDownloadContainer = () => { - const [plugin_list, set_plugin_list] = useState([]); - const [download_progress, set_download_progress] = useState({}); - const { downloadAndExtractPlugin } = usePlugins(); + const { + downloadAndExtractPlugin, + currentPluginsInfoList, + updatePluginsInfoList, + } = usePlugins(); - useEffect(() => { - async function asyncFetchPluginInfoList() { - try { - // tauriFetch を使用して vrct_plugins_list.json を取得(CORS 対策) - const response = await tauriFetch(PLUGIN_LIST_URL, { - method: "GET", - responseType: ResponseType.Json, - headers: { "Cache-Control": "no-cache" } - }); - if (response.status !== 200) { - throw new Error("Failed to fetch plugin list, status: " + response.status); + const downloadStartFunction = async (plugin) => { + updatePluginsInfoList((old_value) => { + const new_value = old_value.data.map(d => { + if (d.plugin_id === plugin.plugin_id) { + d.is_pending = true; } - // 取得される plugin_list.json は各プラグインの plugin_info.json への raw URL の配列とする - const plugins_data = response.data; - const updated_list = await Promise.all( - plugins_data.map(async (plugin_data) => { - try { - const plugin_manifest = await asyncFetchPluginManifest(plugin_data.url); - return { ...plugin_manifest }; - } catch (error) { - console.error("Error fetching manifest for URL:", plugin_data.url, error); - // エラー発生時は、plugin_data.title とエラーメッセージを返す - return { - title: plugin_data.title, - plugin_id: plugin_data.plugin_id || plugin_data.title, - error: error.message, - url: plugin_data.url - }; - } - }) - ); - set_plugin_list(updated_list); - } catch (error) { - console.error("Error fetching plugin info list:", error); - } - } - asyncFetchPluginInfoList(); - }, []); - - const handleDownload = async (plugin) => { + return d; + }); + return new_value; + }); await downloadAndExtractPlugin(plugin); + updatePluginsInfoList((old_value) => { + const new_value = old_value.data.map(d => { + if (d.plugin_id === plugin.plugin_id) { + d.is_pending = false; + } + return d; + }); + return new_value; + }); }; + const plugin_list = currentPluginsInfoList.data; + // const plugin_list = [ + // { + // title: "VRCT Example Plugins 1", + // plugin_id: "vrct_plugin_example_1", + // asset_name: "vrct_plugin_example_1.zip", + // plugin_version: "0.0.6", + // min_supported_vrct_version: "3.0.4", + // max_supported_vrct_version: "3.0.6", + // is_plugin_supported: true, + // // url: manifest_url + // }, + // { + // title: "VRCT Example Plugins 2", + // plugin_id: "vrct_plugin_example_2", + // asset_name: "vrct_plugin_example_2.zip", + // plugin_version: "0.0.1", + // min_supported_vrct_version: "3.0.4", + // max_supported_vrct_version: "3.0.7", + // is_plugin_supported: true, + // // url: manifest_url + // }, + // ]; + + + + return ( -
+
{plugin_list.map((plugin) => ( -
-

{plugin.title}

-

{plugin.plugin_id}

+
+

{plugin.title}

+

{plugin.plugin_id}

{plugin.error ? (

Error: {plugin.error}

) : ( - <> -

Version: {plugin.plugin_version}

-

- Compatible: {plugin.min_compatible_version} ~ {plugin.max_compatible_version} -

- - {download_progress[plugin.plugin_id] !== undefined && ( -
- Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}% -
- )} - +
+
+

Version: {plugin.plugin_version}

+

+ Compatible: {plugin.min_supported_vrct_version} ~ {plugin.max_supported_vrct_version} +

+
+ +
)}
))}
); -}; - -// GitHub Releases の latest 情報から plugin_info.json を取得する(tauriFetch を使用) -async function asyncFetchPluginManifest(manifest_url) { - // リリース情報を取得 - const release_response = await tauriFetch(manifest_url, { - method: "GET", - responseType: ResponseType.Json, - headers: { - "Accept": "application/vnd.github+json", - "User-Agent": "VRCTPluginApp" - } - }); - if (release_response.status !== 200) { - throw new Error(`Failed to fetch release info from ${manifest_url}`); - } - const release_data = release_response.data; - // assets 内に plugin_info.json があるかチェック - const manifest_asset = release_data.assets.find(asset => asset.name === "plugin_info.json"); - if (!manifest_asset) { - throw new Error("plugin_info.json not found in release assets"); - } - // plugin_info.json の内容を取得 - const manifest_response = await tauriFetch(manifest_asset.browser_download_url, { - method: "GET", - responseType: ResponseType.Json, - headers: { - "Accept": "application/json", - "User-Agent": "VRCTPluginApp", - "Cache-Control": "no-cache" - } - }); - if (manifest_response.status !== 200) { - throw new Error(`Failed to fetch plugin_info.json from ${manifest_asset.browser_download_url}`); - } - const plugin_manifest = manifest_response.data; - return { - title: plugin_manifest.title, - plugin_id: plugin_manifest.plugin_id, - plugin_version: plugin_manifest.plugin_version, - min_compatible_version: plugin_manifest.min_compatible_version, - max_compatible_version: plugin_manifest.max_compatible_version, - asset_name: plugin_manifest.asset_name, - url: manifest_url - }; -} - -export { PluginDownloadContainer }; - - - - // // プラグインのマニフェスト(plugin.json から取得した情報の例) - // const plugin_manifest = { - // compatible_lower_version: "3.0.4", - // compatible_upper_version: "3.0.6", - // // 他の情報... - // }; - - // 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); - // }; - - // if (isPluginCompatible(currentSoftwareVersion.data, plugin_manifest.compatible_lower_version, plugin_manifest.compatible_upper_version)) { - // console.log("プラグインは互換性があります。"); - // } else { - // console.error("プラグインは現在の VRCT バージョンと互換性がありません。"); - // } \ No newline at end of file +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss index a49fed11..70eb0860 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss @@ -2,4 +2,34 @@ 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; + flex-direction: column; + padding: 2rem; + &:not(:last-child) { + border-bottom: 0.1rem solid var(--dark_750_color); + } +} + +.title { + font-size: 1.6rem; +} + +.plugin_id { + font-size: 1rem; +} + +.download_button { + background-color: var(--dark_750_color); + padding: 0.4rem 0.6rem; + font-size: 1.2rem; } \ No newline at end of file diff --git a/src-ui/app/main_page/main_section/PluginHost.jsx b/src-ui/app/main_page/main_section/PluginHost.jsx index 8fc162ff..d4d50774 100644 --- a/src-ui/app/main_page/main_section/PluginHost.jsx +++ b/src-ui/app/main_page/main_section/PluginHost.jsx @@ -1,10 +1,8 @@ -// PluginHost.jsx import React from "react"; -import { useStore_LoadedPluginsList } from "@store"; +import { usePlugins } from "@logics_configs"; -// export const PluginHost = ({ location }) => { export const PluginHost = () => { - const { currentLoadedPluginsList } = useStore_LoadedPluginsList(); + const { currentLoadedPluginsList } = usePlugins(); console.log(currentLoadedPluginsList.data); return ( diff --git a/src-ui/logics/common/index.js b/src-ui/logics/common/index.js index 035f844b..5feee60e 100644 --- a/src-ui/logics/common/index.js +++ b/src-ui/logics/common/index.js @@ -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..af9c82f6 --- /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"); + + 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/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index b9ebc97c..4e385366 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,16 +1,31 @@ -import { invoke } from '@tauri-apps/api/tauri'; -import { createAtomWithHook, useStore_LoadedPluginsList } from "@store"; -import { useSoftwareVersion } from "@logics_configs"; +import semver from "semver"; + +import { invoke } from "@tauri-apps/api/tauri"; +import { + createAtomWithHook, + useStore_LoadedPluginsList, + useStore_PluginsInfoList, +} from "@store"; + import { transform } from "@babel/standalone"; import { writeFile, createDir, exists, removeDir, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); import JSZip from "jszip"; -import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; + +import { useFetch } from "@logics_common"; +import { useSoftwareVersion } from "@logics_configs"; + + +// PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL +const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"; export const usePlugins = () => { - const { updateLoadedPluginsList } = useStore_LoadedPluginsList(); + const { currentLoadedPluginsList, updateLoadedPluginsList } = useStore_LoadedPluginsList(); + const { currentPluginsInfoList, updatePluginsInfoList, pendingPluginsInfoList } = useStore_PluginsInfoList(); const { currentSoftwareVersion } = useSoftwareVersion(); + const { asyncTauriFetchGithub } = useFetch(); + const plugin_context = { registerComponent: ({ plugin_id, location, component }) => { if (!plugin_id || !location || !component) { @@ -137,17 +152,9 @@ export const usePlugins = () => { } }; - // GitHub API を使用して、最新リリース情報から asset_name に一致するアセットのブラウザダウンロード URL を返す const fetchLatestPluginZipUrl = async (plugin) => { const api_url = plugin.url; - const response = await tauriFetch(api_url, { - method: "GET", - responseType: ResponseType.Json, - headers: { - "Accept": "application/vnd.github+json", - "User-Agent": "VRCTPluginApp" - } - }); + const response = await asyncTauriFetchGithub(api_url); if (response.status !== 200) { throw new Error("Failed to fetch latest release info, status: " + response.status); } @@ -159,9 +166,87 @@ export const usePlugins = () => { return asset.browser_download_url; }; + + const asyncUpdatePluginInfoList = async () => { + pendingPluginsInfoList(); + try { + const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL); + if (response.status !== 200) { + throw new Error("Failed to fetch plugin 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); + // エラー発生時は、plugin_data.title とエラーメッセージを返す + return { + title: plugin_data.title, + plugin_id: plugin_data.plugin_id || plugin_data.title, + error: error.message, + url: plugin_data.url + }; + } + }) + ); + updatePluginsInfoList(updated_list); + } catch (error) { + console.error("Error fetching plugin info list:", error); + } + } + + 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 isPluginCompatible = (main_version, lower_version, upper_version) => { + console.log(main_version, lower_version, upper_version); + + // lower_version 以上かつ upper_version 以下なら互換性ありと判定 + return semver.gte(main_version, lower_version) && semver.lte(main_version, upper_version); + }; + + const is_plugin_supported = isPluginCompatible(currentSoftwareVersion.data, plugin_info.min_supported_vrct_version, plugin_info.max_supported_vrct_version); + + return { + title: plugin_info.title, + plugin_id: plugin_info.plugin_id, + plugin_version: plugin_info.plugin_version, + min_supported_vrct_version: plugin_info.min_supported_vrct_version, + max_supported_vrct_version: plugin_info.max_supported_vrct_version, + is_plugin_supported: is_plugin_supported, + asset_name: plugin_info.asset_name, + url: plugin_info_asset_url + }; + } + + + return { + asyncUpdatePluginInfoList, + loadAllPlugins, downloadAndExtractPlugin, + + currentLoadedPluginsList, + updateLoadedPluginsList, + + currentPluginsInfoList, + updatePluginsInfoList, }; }; diff --git a/src-ui/store.js b/src-ui/store.js index da4c1f45..d35f02aa 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -275,8 +275,8 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA }, "Hotkeys"); // Plugins -export const { atomInstance: Atom_InstalledPluginsPath, useHook: useStore_InstalledPluginsPath } = createAtomWithHook([], "InstalledPluginsPath"); export const { atomInstance: Atom_LoadedPluginsList, useHook: useStore_LoadedPluginsList } = createAtomWithHook([], "LoadedPluginsList"); +export const { atomInstance: Atom_PluginsInfoList, useHook: useStore_PluginsInfoList } = createAtomWithHook([], "PluginsInfoList"); // Advanced Settings export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress"); From 3ebd60509236d44607a1a8ff0b1e2667f372da7d Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 14 Mar 2025 23:22:00 +0900 Subject: [PATCH 10/47] [Update/TMP] Save plugin status that if enabled or not(on store.js, yet). Handle download. --- .../_DownloadButton.module.scss | 1 + .../download_plugins/DownloadPlugins.jsx | 24 ++++---- .../DownloadPlugins.module.scss | 7 +++ .../setting_box/plugins/Plugins.jsx | 55 +++++++++++++++---- .../setting_box/plugins/Plugins.module.scss | 7 +++ .../app/main_page/main_section/PluginHost.jsx | 2 +- src-ui/logics/common/useFetch.js | 2 +- src-ui/logics/configs/plugins/usePlugins.js | 5 ++ src-ui/store.js | 4 ++ 9 files changed, 82 insertions(+), 25 deletions(-) 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 index 59fabfe3..6b42ca5d 100644 --- 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 @@ -13,6 +13,7 @@ background-color: var(--dark_800_color); padding: 0.8rem; flex-shrink: 0; + border-radius: 0.2rem; &:hover { background-color: var(--dark_750_color); } diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx index 7c1a17b9..1e66b9c7 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx @@ -2,30 +2,32 @@ import { SwitchBox, } from "../index"; import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; +import styles from "./DownloadPlugins.module.scss"; -export const DownloadPlugins = ({plugin_info, ...props}) => { - +export const DownloadPlugins = ({plugin_info, plugin_status, ...props}) => { const option = { + id: plugin_info.plugin_id, is_pending: plugin_info.is_pending, is_downloaded: plugin_info.is_downloaded, - } - console.log(plugin_info); + progress: null, + }; return ( -
- {/* */} +
+ {plugin_info.is_downloaded && plugin_info.is_plugin_supported && + } {plugin_info.is_plugin_supported ? <_DownloadButton option={option} downloadStartFunction={props.downloadStartFunction} /> : -
- Unavailable +
+ Downloaded but outdated.
}
diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss index e69de29b..99a37516 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss @@ -0,0 +1,7 @@ +.container { +} + +.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/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 8201cdd8..ea96223c 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -16,31 +16,41 @@ const PluginDownloadContainer = () => { downloadAndExtractPlugin, currentPluginsInfoList, updatePluginsInfoList, + currentSavedPluginsStatus, + updateSavedPluginsStatus, + currentLoadedPluginsList, + // updateLoadedPluginsList, } = usePlugins(); - const downloadStartFunction = async (plugin) => { + const downloadStartFunction = async (target_plugin_id) => { updatePluginsInfoList((old_value) => { const new_value = old_value.data.map(d => { - if (d.plugin_id === plugin.plugin_id) { + if (d.plugin_id === target_plugin_id) { d.is_pending = true; } return d; }); return new_value; }); - await downloadAndExtractPlugin(plugin); - updatePluginsInfoList((old_value) => { - const new_value = old_value.data.map(d => { - if (d.plugin_id === plugin.plugin_id) { - d.is_pending = false; - } - return d; + const target_plugin_info = currentPluginsInfoList.data.find(d => d.plugin_id === target_plugin_id); + downloadAndExtractPlugin(target_plugin_info).then(() => { + updatePluginsInfoList((old_value) => { + const new_value = old_value.data.map(d => { + if (d.plugin_id === target_plugin_id) { + d.is_pending = false; + d.is_downloaded = true; + } + return d; + }); + return new_value; }); - return new_value; - }); + }) }; + // console.log(currentPluginsInfoList.data); - const plugin_list = currentPluginsInfoList.data; + + // const plugin_list = currentPluginsInfoList.data; + const plugin_list = [...currentPluginsInfoList.data, ...currentLoadedPluginsList.data]; // const plugin_list = [ // { // title: "VRCT Example Plugins 1", @@ -64,6 +74,26 @@ const PluginDownloadContainer = () => { // }, // ]; + const getTargetPluginStatus = (target_plugin_id) => { + let plugin_status = currentSavedPluginsStatus.data.find(d => d.plugin_id === target_plugin_id) ?? {}; + const is_downloaded = currentLoadedPluginsList.data.find(d => d.plugin_id === target_plugin_id) ? true : false; + + plugin_status.toggleFunction = () => { + updateSavedPluginsStatus((old_value) => { + const new_value = old_value.data.map(d => { + if (d.plugin_id === target_plugin_id) { + d.data = !d.data; + d.state = "ok"; + } + return d; + }); + return new_value; + }); + } + plugin_status.is_downloaded = is_downloaded; + plugin_status.is_pending = false; + return plugin_status; + }; @@ -85,6 +115,7 @@ const PluginDownloadContainer = () => {
diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss index 70eb0860..3320d590 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss @@ -20,6 +20,13 @@ } } +.plugin_info_wrapper { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; +} + .title { font-size: 1.6rem; } diff --git a/src-ui/app/main_page/main_section/PluginHost.jsx b/src-ui/app/main_page/main_section/PluginHost.jsx index d4d50774..264bd579 100644 --- a/src-ui/app/main_page/main_section/PluginHost.jsx +++ b/src-ui/app/main_page/main_section/PluginHost.jsx @@ -3,7 +3,7 @@ import { usePlugins } from "@logics_configs"; export const PluginHost = () => { const { currentLoadedPluginsList } = usePlugins(); - console.log(currentLoadedPluginsList.data); + // console.log(currentLoadedPluginsList.data); return (
diff --git a/src-ui/logics/common/useFetch.js b/src-ui/logics/common/useFetch.js index af9c82f6..12ba5def 100644 --- a/src-ui/logics/common/useFetch.js +++ b/src-ui/logics/common/useFetch.js @@ -2,7 +2,7 @@ import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; export const useFetch = () => { const asyncTauriFetchGithub = async (url) => { - console.log("tauriFetch"); + console.log("tauriFetch", url); const release_response = await tauriFetch(url, { method: "GET", diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 4e385366..c8c18749 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/tauri"; import { createAtomWithHook, useStore_LoadedPluginsList, + useStore_SavedPluginsStatus, useStore_PluginsInfoList, } from "@store"; @@ -21,6 +22,7 @@ const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_p export const usePlugins = () => { const { currentLoadedPluginsList, updateLoadedPluginsList } = useStore_LoadedPluginsList(); + const { currentSavedPluginsStatus, updateSavedPluginsStatus } = useStore_SavedPluginsStatus(); const { currentPluginsInfoList, updatePluginsInfoList, pendingPluginsInfoList } = useStore_PluginsInfoList(); const { currentSoftwareVersion } = useSoftwareVersion(); @@ -245,6 +247,9 @@ export const usePlugins = () => { currentLoadedPluginsList, updateLoadedPluginsList, + currentSavedPluginsStatus, + updateSavedPluginsStatus, + currentPluginsInfoList, updatePluginsInfoList, }; diff --git a/src-ui/store.js b/src-ui/store.js index d35f02aa..2da4c128 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -276,6 +276,10 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA // Plugins export const { atomInstance: Atom_LoadedPluginsList, useHook: useStore_LoadedPluginsList } = createAtomWithHook([], "LoadedPluginsList"); +export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([ + { plugin_id: "vrct_plugin_example_1", is_enabled: true }, + { plugin_id: "vrct_plugin_example_2", is_enabled: false } +], "SavedPluginsStatus"); export const { atomInstance: Atom_PluginsInfoList, useHook: useStore_PluginsInfoList } = createAtomWithHook([], "PluginsInfoList"); // Advanced Settings From 0148f9bee0938cbd90a5e6f4cf46c57e3e1d1084 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:17:04 +0900 Subject: [PATCH 11/47] [Update] (Affect to backend) Plugin System. Saveable if the plugin is enabled or not. Add functions that merge plugins data. --- src-python/config.py | 13 +++ src-python/controller.py | 9 ++ src-python/mainloop.py | 3 + src-ui/app/App.jsx | 8 +- .../_app_controllers/PluginsController.jsx | 77 +++++++++++++-- .../download_plugins/DownloadPlugins.jsx | 35 ------- .../setting_box/_components/index.js | 2 +- .../PluginsControlComponent.jsx | 48 ++++++++++ .../PluginsControlComponent.module.scss} | 0 .../switch_box/SwitchBox.module.scss | 1 + .../setting_box/plugins/Plugins.jsx | 95 ++++++++----------- .../app/main_page/main_section/PluginHost.jsx | 7 +- src-ui/logics/configs/plugins/usePlugins.js | 50 ++++++---- src-ui/logics/useReceiveRoutes.js | 6 ++ src-ui/store.js | 9 +- 15 files changed, 229 insertions(+), 134 deletions(-) delete mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.jsx rename src-ui/app/config_page/setting_section/setting_box/_components/{download_plugins/DownloadPlugins.module.scss => plugins_control_component/PluginsControlComponent.module.scss} (100%) diff --git a/src-python/config.py b/src-python/config.py index b607863b..8038b047 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 f0356287..02ba5e34 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -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..edd5356a 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -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-ui/app/App.jsx b/src-ui/app/App.jsx index 22c5107c..269bf824 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -1,3 +1,4 @@ +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { @@ -29,6 +30,7 @@ export const App = () => { const { currentIsBackendReady } = useIsBackendReady(); const { WindowGeometryController } = useWindow(); const { i18n } = useTranslation(); + const fetchPluginsHasRunRef = useRef(false); return (
@@ -44,7 +46,7 @@ export const App = () => { {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) ? - : + : } @@ -52,11 +54,11 @@ export const App = () => { ); }; -const Contents = () => { +const Contents = ({ fetchPluginsHasRunRef }) => { const { currentIsSoftwareUpdating } = useIsSoftwareUpdating(); return ( <> - + diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 16305224..d6f97ab4 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -5,21 +5,80 @@ if (typeof window !== "undefined") { window.React = React; } -export const PluginsController = () => { - const hasRunRef = useRef(false); +export const PluginsController = ({ fetchPluginsHasRunRef }) => { const { - loadAllPlugins, - asyncUpdatePluginInfoList, + asyncLoadAllPlugins, + asyncFetchPluginsInfo, + currentPluginsData, + updatePluginsData, + currentSavedPluginsStatus, } = usePlugins(); useEffect(() => { - if (!hasRunRef.current) { - asyncUpdatePluginInfoList().then(() => { - loadAllPlugins(); - }); + const loadPlugins = async () => { + try { + await asyncLoadAllPlugins(); + const info_array = await asyncFetchPluginsInfo(); + updatePluginsData(prev => { + // Map を利用してそれぞれの配列を plugin_id で参照できるようにする + const infoMap = new Map(info_array.map(info => [info.plugin_id, info])); + const prevMap = new Map(prev.data.map(item => [item.plugin_id, item])); + + // info_array にある各アイテムについて、prev.data に同じ plugin_id があればマージ + const merged = info_array.map(info => { + if (prevMap.has(info.plugin_id)) { + return { ...info, ...prevMap.get(info.plugin_id) }; + } + return info; + }); + + // prev.data にのみ存在するアイテムを追加し、is_outdated: true を付与 + prev.data.forEach(item => { + if (!infoMap.has(item.plugin_id)) { + merged.push({ ...item, is_outdated: true }); + } + }); + + return merged; + }); + } catch (error) { + console.error(error); + } + }; + + if (!fetchPluginsHasRunRef.current) { + loadPlugins(); } - return () => hasRunRef.current = true; + return () => fetchPluginsHasRunRef.current = true; }, []); + + useEffect(() => { + updatePluginsData(prev => { + // currentSavedPluginsStatus.data の各要素を Map 化して plugin_id で参照 + const savedMap = new Map(currentSavedPluginsStatus.data.map(saved => [saved.plugin_id, saved])); + const prevMap = new Map(prev.data.map(item => [item.plugin_id, item])); + + // prev.data にある各アイテムについて、保存済みの状態情報があればマージ + const merged = prev.data.map(item => { + if (savedMap.has(item.plugin_id)) { + return { ...item, is_enabled: savedMap.get(item.plugin_id).is_enabled }; + } + return item; + }); + + // currentSavedPluginsStatus.data にのみ存在する項目があれば追加 + currentSavedPluginsStatus.data.forEach(saved => { + if (!prevMap.has(saved.plugin_id)) { + merged.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); + } + }); + + return merged; + }); + }, [currentSavedPluginsStatus]); + + + return null; }; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx deleted file mode 100644 index 1e66b9c7..00000000 --- a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - SwitchBox, -} from "../index"; -import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; -import styles from "./DownloadPlugins.module.scss"; - -export const DownloadPlugins = ({plugin_info, plugin_status, ...props}) => { - const option = { - id: plugin_info.plugin_id, - is_pending: plugin_info.is_pending, - is_downloaded: plugin_info.is_downloaded, - progress: null, - }; - - - return ( -
- {plugin_info.is_downloaded && plugin_info.is_plugin_supported && - } - {plugin_info.is_plugin_supported ? - <_DownloadButton - option={option} - downloadStartFunction={props.downloadStartFunction} - /> - : -
- Downloaded but outdated. -
- } -
- ); -}; \ No newline at end of file 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 33b0837d..123a1400 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,4 @@ 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"; -export { DownloadPlugins } from "./download_plugins/DownloadPlugins"; \ No newline at end of file +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..33ca0fa7 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.jsx @@ -0,0 +1,48 @@ +import { + SwitchBox, +} from "../index"; +import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; +import styles from "./PluginsControlComponent.module.scss"; + +export const PluginsControlComponent = ({ variable_state, plugin_status, toggleFunction, ...props }) => { + const option = { + id: plugin_status.plugin_id, + is_pending: plugin_status.is_pending, + is_downloaded: plugin_status.is_downloaded, + data: plugin_status.is_enabled, + state: variable_state, + progress: null, + }; + + const adjustedToggleFunction = () => { + toggleFunction(plugin_status.plugin_id); + }; + + let is_turn_on_able = false; + if (plugin_status.is_downloaded && plugin_status.is_plugin_supported) { + is_turn_on_able = true; + } + if (plugin_status.is_outdated) { + is_turn_on_able = true; + } + + return ( +
+ {is_turn_on_able && + } + {plugin_status.is_plugin_supported ? + <_DownloadButton + option={option} + downloadStartFunction={props.downloadStartFunction} + /> + : +
+ Downloaded but outdated. +
+ } +
+ ); +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.module.scss similarity index 100% rename from src-ui/app/config_page/setting_section/setting_box/_components/download_plugins/DownloadPlugins.module.scss rename to src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.module.scss 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..29c26df5 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 @@ -22,6 +22,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/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index ea96223c..cddda330 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { usePlugins } from "@logics_configs"; import styles from "./Plugins.module.scss"; -import { DownloadPlugins } from "../_components"; +import { PluginsControlComponent } from "../_components"; export const Plugins = () => { return ( @@ -14,16 +14,15 @@ export const Plugins = () => { const PluginDownloadContainer = () => { const { downloadAndExtractPlugin, - currentPluginsInfoList, - updatePluginsInfoList, + currentPluginsData, + updatePluginsData, currentSavedPluginsStatus, - updateSavedPluginsStatus, - currentLoadedPluginsList, - // updateLoadedPluginsList, + setSavedPluginsStatus, } = usePlugins(); + const downloadStartFunction = async (target_plugin_id) => { - updatePluginsInfoList((old_value) => { + updatePluginsData((old_value) => { const new_value = old_value.data.map(d => { if (d.plugin_id === target_plugin_id) { d.is_pending = true; @@ -32,9 +31,9 @@ const PluginDownloadContainer = () => { }); return new_value; }); - const target_plugin_info = currentPluginsInfoList.data.find(d => d.plugin_id === target_plugin_id); + const target_plugin_info = currentPluginsData.data.find(d => d.plugin_id === target_plugin_id); downloadAndExtractPlugin(target_plugin_info).then(() => { - updatePluginsInfoList((old_value) => { + updatePluginsData((old_value) => { const new_value = old_value.data.map(d => { if (d.plugin_id === target_plugin_id) { d.is_pending = false; @@ -46,60 +45,41 @@ const PluginDownloadContainer = () => { }); }) }; - // console.log(currentPluginsInfoList.data); - // const plugin_list = currentPluginsInfoList.data; - const plugin_list = [...currentPluginsInfoList.data, ...currentLoadedPluginsList.data]; - // const plugin_list = [ - // { - // title: "VRCT Example Plugins 1", - // plugin_id: "vrct_plugin_example_1", - // asset_name: "vrct_plugin_example_1.zip", - // plugin_version: "0.0.6", - // min_supported_vrct_version: "3.0.4", - // max_supported_vrct_version: "3.0.6", - // is_plugin_supported: true, - // // url: manifest_url - // }, - // { - // title: "VRCT Example Plugins 2", - // plugin_id: "vrct_plugin_example_2", - // asset_name: "vrct_plugin_example_2.zip", - // plugin_version: "0.0.1", - // min_supported_vrct_version: "3.0.4", - // max_supported_vrct_version: "3.0.7", - // is_plugin_supported: true, - // // url: manifest_url - // }, - // ]; - - const getTargetPluginStatus = (target_plugin_id) => { - let plugin_status = currentSavedPluginsStatus.data.find(d => d.plugin_id === target_plugin_id) ?? {}; - const is_downloaded = currentLoadedPluginsList.data.find(d => d.plugin_id === target_plugin_id) ? true : false; - - plugin_status.toggleFunction = () => { - updateSavedPluginsStatus((old_value) => { - const new_value = old_value.data.map(d => { - if (d.plugin_id === target_plugin_id) { - d.data = !d.data; - d.state = "ok"; - } - return d; - }); - return new_value; + const toggleFunction = (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; + } + return d; + }); + } else { + new_value.push(...currentSavedPluginsStatus.data); + new_value.push({ + plugin_id: target_plugin_id, + is_enabled: true, }); } - plugin_status.is_downloaded = is_downloaded; - plugin_status.is_pending = false; - return plugin_status; - }; + // currentPluginsData.data で、is_downloaded が true のものだけ残す + new_value = new_value.filter(item => { + return currentPluginsData.data.some(plugin => plugin.plugin_id === item.plugin_id && plugin.is_downloaded); + }); + + setSavedPluginsStatus(new_value); + } + + const variable_state = currentSavedPluginsStatus.state; + return (
- {plugin_list.map((plugin) => ( + {currentPluginsData.data.map((plugin) => (

{plugin.title}

{plugin.plugin_id}

@@ -113,9 +93,10 @@ const PluginDownloadContainer = () => { Compatible: {plugin.min_supported_vrct_version} ~ {plugin.max_supported_vrct_version}

-
diff --git a/src-ui/app/main_page/main_section/PluginHost.jsx b/src-ui/app/main_page/main_section/PluginHost.jsx index 264bd579..5c01328c 100644 --- a/src-ui/app/main_page/main_section/PluginHost.jsx +++ b/src-ui/app/main_page/main_section/PluginHost.jsx @@ -2,13 +2,12 @@ import React from "react"; import { usePlugins } from "@logics_configs"; export const PluginHost = () => { - const { currentLoadedPluginsList } = usePlugins(); - // console.log(currentLoadedPluginsList.data); + const { currentPluginsData } = usePlugins(); return (
- {currentLoadedPluginsList.data - .filter((plugin) => plugin.location === "main_section") + {currentPluginsData.data + .filter((plugin) => plugin.is_enabled && plugin.location === "main_section") .map((plugin, index) => { const PluginComponent = plugin.component; return PluginComponent ? : null; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index c8c18749..7bc55256 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -3,10 +3,10 @@ import semver from "semver"; import { invoke } from "@tauri-apps/api/tauri"; import { createAtomWithHook, - useStore_LoadedPluginsList, useStore_SavedPluginsStatus, - useStore_PluginsInfoList, + useStore_PluginsData, } 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"; @@ -21,9 +21,10 @@ import { useSoftwareVersion } from "@logics_configs"; const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"; export const usePlugins = () => { - const { currentLoadedPluginsList, updateLoadedPluginsList } = useStore_LoadedPluginsList(); - const { currentSavedPluginsStatus, updateSavedPluginsStatus } = useStore_SavedPluginsStatus(); - const { currentPluginsInfoList, updatePluginsInfoList, pendingPluginsInfoList } = useStore_PluginsInfoList(); + const { asyncStdoutToPython } = useStdoutToPython(); + // const { currentLoadedPluginsList, updateLoadedPluginsList } = useStore_LoadedPluginsList(); + const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); + const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); const { currentSoftwareVersion } = useSoftwareVersion(); const { asyncTauriFetchGithub } = useFetch(); @@ -33,9 +34,17 @@ export const usePlugins = () => { if (!plugin_id || !location || !component) { return console.error("An invalid plugin was detected.", plugin_id, location, component); } - updateLoadedPluginsList((prev) => { - const filtered = prev.data.filter(item => item.plugin_id !== plugin_id); - return [...filtered, { plugin_id, location, component }]; + updatePluginsData(prev => { + const is_already_registered = prev.data.some(old_value => old_value.plugin_id === plugin_id); + const new_value = prev.data.map(old_value => + old_value.plugin_id === plugin_id + ? { ...old_value, location, component, is_downloaded: true } + : old_value + ); + + return is_already_registered + ? new_value + : [...new_value, { plugin_id, location, component, is_downloaded: true }]; }); }, createAtomWithHook: (...args) => createAtomWithHook(...args) @@ -66,7 +75,7 @@ export const usePlugins = () => { } }; - const loadAllPlugins = async () => { + const asyncLoadAllPlugins = async () => { if (import.meta.env.DEV) { // 開発時: ホットリロード対応、src-tauri以下のpluginsから直接読み込み Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => { @@ -169,8 +178,7 @@ export const usePlugins = () => { }; - const asyncUpdatePluginInfoList = async () => { - pendingPluginsInfoList(); + const asyncFetchPluginsInfo = async () => { try { const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL); if (response.status !== 200) { @@ -194,7 +202,7 @@ export const usePlugins = () => { } }) ); - updatePluginsInfoList(updated_list); + return updated_list; } catch (error) { console.error("Error fetching plugin info list:", error); } @@ -236,22 +244,26 @@ export const usePlugins = () => { }; } + const setSavedPluginsStatus = (plugins_status) => { + pendingSavedPluginsStatus(); + asyncStdoutToPython("/set/data/plugins_status", plugins_status); + }; + return { - asyncUpdatePluginInfoList, + asyncFetchPluginsInfo, - loadAllPlugins, + asyncLoadAllPlugins, downloadAndExtractPlugin, - currentLoadedPluginsList, - updateLoadedPluginsList, - currentSavedPluginsStatus, updateSavedPluginsStatus, - currentPluginsInfoList, - updatePluginsInfoList, + currentPluginsData, + updatePluginsData, + + setSavedPluginsStatus, }; }; diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index 53a2feb3..4575c3d8 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -73,6 +73,7 @@ import { useOverlayShowOnlyTranslatedMessages, useEnableNotificationVrcSfx, useHotkeys, + usePlugins, useOscIpAddress, useOscPort, } from "@logics_configs"; @@ -176,6 +177,7 @@ export const useReceiveRoutes = () => { const { updateEnableNotificationVrcSfx } = useEnableNotificationVrcSfx(); const { updateHotkeys } = useHotkeys(); + const { updateSavedPluginsStatus } = usePlugins(); const { updateOscIpAddress } = useOscIpAddress(); const { updateOscPort } = useOscPort(); @@ -488,6 +490,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/store.js b/src-ui/store.js index 2da4c128..410a3809 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -275,12 +275,9 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA }, "Hotkeys"); // Plugins -export const { atomInstance: Atom_LoadedPluginsList, useHook: useStore_LoadedPluginsList } = createAtomWithHook([], "LoadedPluginsList"); -export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([ - { plugin_id: "vrct_plugin_example_1", is_enabled: true }, - { plugin_id: "vrct_plugin_example_2", is_enabled: false } -], "SavedPluginsStatus"); -export const { atomInstance: Atom_PluginsInfoList, useHook: useStore_PluginsInfoList } = createAtomWithHook([], "PluginsInfoList"); +// export const { atomInstance: Atom_LoadedPluginsList, useHook: useStore_LoadedPluginsList } = createAtomWithHook([], "LoadedPluginsList"); +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"); From e65e9be052225aada1d675149a1b066ebed00e4b Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:35:17 +0900 Subject: [PATCH 12/47] [Update] Add plugin_info.json load function. --- src-tauri/plugins/plugin_examples/index.jsx | 1 - .../plugins/plugin_examples/plugin_info.json | 8 ++ src-ui/logics/configs/plugins/usePlugins.js | 77 ++++++++++++------- 3 files changed, 56 insertions(+), 30 deletions(-) create mode 100644 src-tauri/plugins/plugin_examples/plugin_info.json diff --git a/src-tauri/plugins/plugin_examples/index.jsx b/src-tauri/plugins/plugin_examples/index.jsx index 58441bd8..64c0fe86 100644 --- a/src-tauri/plugins/plugin_examples/index.jsx +++ b/src-tauri/plugins/plugin_examples/index.jsx @@ -9,7 +9,6 @@ export const init = (plugin_context) => { }; plugin_context.registerComponent({ - plugin_id: "plugin_example_1_my_plugin", location: "main_section", component: EntryComponents, }); diff --git a/src-tauri/plugins/plugin_examples/plugin_info.json b/src-tauri/plugins/plugin_examples/plugin_info.json new file mode 100644 index 00000000..ffcd0a76 --- /dev/null +++ b/src-tauri/plugins/plugin_examples/plugin_info.json @@ -0,0 +1,8 @@ +{ + "title": "VRCT Example Plugins 1", + "plugin_id": "vrct_plugin_example_1", + "asset_name": "vrct_plugin_example_1.zip", + "plugin_version": "0.0.1", + "min_supported_vrct_version": "3.0.4", + "max_supported_vrct_version": "3.0.6" +} \ No newline at end of file diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 7bc55256..2438aeef 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -11,6 +11,7 @@ import { useStdoutToPython } from "@logics/useStdoutToPython"; import { transform } from "@babel/standalone"; import { writeFile, createDir, exists, removeDir, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); +const dev_plugin_info_mapping = import.meta.glob("/src-tauri/plugins/**/plugin_info.json", { eager: true }); import JSZip from "jszip"; import { useFetch } from "@logics_common"; @@ -29,31 +30,37 @@ export const usePlugins = () => { const { asyncTauriFetchGithub } = useFetch(); - const plugin_context = { - registerComponent: ({ plugin_id, location, component }) => { - if (!plugin_id || !location || !component) { - return console.error("An invalid plugin was detected.", plugin_id, location, component); - } - updatePluginsData(prev => { - const is_already_registered = prev.data.some(old_value => old_value.plugin_id === plugin_id); - const new_value = prev.data.map(old_value => - old_value.plugin_id === plugin_id - ? { ...old_value, location, component, is_downloaded: true } - : old_value - ); + const generatePluginContext= (plugin_info) => { + const plugin_context = { + registerComponent: ({ location, component }) => { + if (!plugin_info.plugin_id || !location || !component) { + return console.error("An invalid plugin was detected.", plugin_info.plugin_id, location, component); + } + updatePluginsData(prev => { + const is_already_registered = prev.data.some(old_value => old_value.plugin_id === plugin_info.plugin_id); - return is_already_registered - ? new_value - : [...new_value, { plugin_id, location, component, is_downloaded: true }]; - }); - }, - createAtomWithHook: (...args) => createAtomWithHook(...args) - }; + const new_value = prev.data.map(old_value => + old_value.plugin_id === plugin_info.plugin_id + ? { ...old_value, ...plugin_info, location, component, is_downloaded: true } + : old_value + ); - const asyncLoadPlugin = async (plugin_relative_path) => { - plugin_relative_path = "plugins/" + plugin_relative_path; + return is_already_registered + ? new_value + : [...new_value, { plugin_id: plugin_info.plugin_id, location, component, is_downloaded: true }]; + }); + }, + createAtomWithHook: (...args) => createAtomWithHook(...args) + }; + return plugin_context; + } + + const asyncLoadPlugin = async (plugin_folder_relative_path) => { + const init_path = "plugins/" + plugin_folder_relative_path +"/index.esm.js"; + const plugin_info_path = "plugins/" + plugin_folder_relative_path +"/plugin_info.json"; try { - const plugin_code = await readTextFile(plugin_relative_path, { dir: BaseDirectory.Resource, recursive: true }); + const plugin_info = await readTextFile(plugin_info_path, { dir: BaseDirectory.Resource, recursive: true }); + 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: [ @@ -68,10 +75,10 @@ export const usePlugins = () => { URL.revokeObjectURL(blob_url); if (plugin_module && plugin_module.init) { - plugin_module.init(plugin_context); + plugin_module.init(generatePluginContext(plugin_info)); } } catch (error) { - console.error("Failed to load plugin from", plugin_relative_path, error); + console.error("Failed to load plugin from", plugin_folder_relative_path, error); } }; @@ -79,6 +86,18 @@ export const usePlugins = () => { if (import.meta.env.DEV) { // 開発時: ホットリロード対応、src-tauri以下のpluginsから直接読み込み Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => { + // 例: key が "/src-tauri/plugins/sample/index.jsx" の場合、plugin_info.json のパスは同じディレクトリ内にある + const pluginInfoKey = key.replace("index.jsx", "plugin_info.json"); + const plugin_info = dev_plugin_info_mapping[pluginInfoKey]; + + if (!plugin_info) { + console.error("plugin_info.json has not found:", pluginInfoKey); + return; + } + + // plugin_info を使ってプラグインコンテキストを生成 + const plugin_context = generatePluginContext(plugin_info); + if (plugin_module && plugin_module.init) { plugin_module.init(plugin_context); } @@ -87,8 +106,8 @@ export const usePlugins = () => { try { const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true }); for (const target_dir of plugin_files) { - const target_path = target_dir.name + "/index.esm.js"; - await asyncLoadPlugin(target_path, plugin_context); + const target_path = target_dir.name; + await asyncLoadPlugin(target_path); } } catch (error) { console.error("Error loading plugins:", error); @@ -113,7 +132,7 @@ export const usePlugins = () => { const zip = await JSZip.loadAsync(bytes); // 展開先ディレクトリのパス(例:"plugins/" とする) - const target_plugin_path = "plugins/" + plugin.asset_name.replace(".zip", ""); + const target_plugin_path = "plugins/" + plugin.plugin_id; // 既に存在する場合は削除してから新規作成 if (await exists(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true })) { await removeDir(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true }); @@ -154,8 +173,8 @@ export const usePlugins = () => { await Promise.all(file_promises); console.log("Plugin downloaded successfully."); - const index_file_relative_path = plugin.asset_name.replace(".zip", "") + "/index.esm.js"; - await asyncLoadPlugin(index_file_relative_path, plugin_context); + const index_file_relative_path = plugin.plugin_id; + await asyncLoadPlugin(index_file_relative_path); console.log("Plugin loaded successfully."); } catch (error) { From 92ea06eb776ab30f25fb9869fb6976cb229d7f15 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:53:52 +0900 Subject: [PATCH 13/47] [bugfix] Fix load json format error. --- src-ui/logics/configs/plugins/usePlugins.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 2438aeef..f0a4073b 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -56,10 +56,12 @@ export const usePlugins = () => { } const asyncLoadPlugin = async (plugin_folder_relative_path) => { - const init_path = "plugins/" + plugin_folder_relative_path +"/index.esm.js"; - const plugin_info_path = "plugins/" + plugin_folder_relative_path +"/plugin_info.json"; + const init_path = "plugins/" + plugin_folder_relative_path + "/index.esm.js"; + const plugin_info_path = "plugins/" + plugin_folder_relative_path + "/plugin_info.json"; try { - const plugin_info = await readTextFile(plugin_info_path, { dir: BaseDirectory.Resource, recursive: true }); + const plugin_info_json = await readTextFile(plugin_info_path, { dir: BaseDirectory.Resource, recursive: true }); + const plugin_info = JSON.parse(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, { From 824a9fa0a9f4c2f9c6949f89d53dbd3761839b2e Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:57:03 +0900 Subject: [PATCH 14/47] [Update] Plugins: Move location from index to plugin_info.json. --- src-tauri/plugins/plugin_examples/index.jsx | 5 +---- src-tauri/plugins/plugin_examples/plugin_info.json | 1 + src-ui/logics/configs/plugins/usePlugins.js | 11 +++++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src-tauri/plugins/plugin_examples/index.jsx b/src-tauri/plugins/plugin_examples/index.jsx index 64c0fe86..4ded2716 100644 --- a/src-tauri/plugins/plugin_examples/index.jsx +++ b/src-tauri/plugins/plugin_examples/index.jsx @@ -8,10 +8,7 @@ export const init = (plugin_context) => { return ; }; - plugin_context.registerComponent({ - location: "main_section", - component: EntryComponents, - }); + plugin_context.registerComponent(EntryComponents); }; export default init; \ No newline at end of file diff --git a/src-tauri/plugins/plugin_examples/plugin_info.json b/src-tauri/plugins/plugin_examples/plugin_info.json index ffcd0a76..303f0ecb 100644 --- a/src-tauri/plugins/plugin_examples/plugin_info.json +++ b/src-tauri/plugins/plugin_examples/plugin_info.json @@ -2,6 +2,7 @@ "title": "VRCT Example Plugins 1", "plugin_id": "vrct_plugin_example_1", "asset_name": "vrct_plugin_example_1.zip", + "location": "main_section", "plugin_version": "0.0.1", "min_supported_vrct_version": "3.0.4", "max_supported_vrct_version": "3.0.6" diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index f0a4073b..984ee189 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -23,7 +23,6 @@ const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_p export const usePlugins = () => { const { asyncStdoutToPython } = useStdoutToPython(); - // const { currentLoadedPluginsList, updateLoadedPluginsList } = useStore_LoadedPluginsList(); const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); const { currentSoftwareVersion } = useSoftwareVersion(); @@ -32,22 +31,22 @@ export const usePlugins = () => { const generatePluginContext= (plugin_info) => { const plugin_context = { - registerComponent: ({ location, component }) => { - if (!plugin_info.plugin_id || !location || !component) { - return console.error("An invalid plugin was detected.", plugin_info.plugin_id, location, component); + registerComponent: (component) => { + if (!plugin_info.plugin_id || !plugin_info.location || !component) { + return console.error("An invalid plugin was detected.", plugin_info.plugin_id, plugin_info.location, component); } updatePluginsData(prev => { const is_already_registered = prev.data.some(old_value => old_value.plugin_id === plugin_info.plugin_id); const new_value = prev.data.map(old_value => old_value.plugin_id === plugin_info.plugin_id - ? { ...old_value, ...plugin_info, location, component, is_downloaded: true } + ? { ...old_value, ...plugin_info, location: plugin_info.location, component, is_downloaded: true } : old_value ); return is_already_registered ? new_value - : [...new_value, { plugin_id: plugin_info.plugin_id, location, component, is_downloaded: true }]; + : [...new_value, { plugin_id: plugin_info.plugin_id, location: plugin_info.location, component, is_downloaded: true }]; }); }, createAtomWithHook: (...args) => createAtomWithHook(...args) From 1ebdefcd43d1adec7545e8d313cf7d1d0ec847f4 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:45:29 +0900 Subject: [PATCH 15/47] [Update/bugfix] Plugins: For development, fix hot reload issue that was restart every time updated the files. --- src-tauri/plugins/index.js | 1 + src-ui/logics/configs/plugins/usePlugins.js | 25 ++++++++----------- src-ui/plugins/index.js | 9 +++++++ .../plugins/plugin_examples/index.jsx | 0 .../main_container/MainContainer.jsx | 0 .../plugins/plugin_examples/plugin_info.json | 0 .../plugins/plugin_examples/store/store.js | 0 vite.config.js | 7 ++---- 8 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 src-tauri/plugins/index.js create mode 100644 src-ui/plugins/index.js rename {src-tauri => src-ui}/plugins/plugin_examples/index.jsx (100%) rename {src-tauri => src-ui}/plugins/plugin_examples/main_container/MainContainer.jsx (100%) rename {src-tauri => src-ui}/plugins/plugin_examples/plugin_info.json (100%) rename {src-tauri => src-ui}/plugins/plugin_examples/store/store.js (100%) 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-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 984ee189..2c337f7c 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -10,8 +10,8 @@ import { useStdoutToPython } from "@logics/useStdoutToPython"; import { transform } from "@babel/standalone"; import { writeFile, createDir, exists, removeDir, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs"; -const dev_plugin_mapping = import.meta.glob("/src-tauri/plugins/**/index.jsx", { eager: true }); -const dev_plugin_info_mapping = import.meta.glob("/src-tauri/plugins/**/plugin_info.json", { eager: true }); +import { dev_plugins } from "@dev_plugins_path"; + import JSZip from "jszip"; import { useFetch } from "@logics_common"; @@ -85,22 +85,17 @@ export const usePlugins = () => { const asyncLoadAllPlugins = async () => { if (import.meta.env.DEV) { - // 開発時: ホットリロード対応、src-tauri以下のpluginsから直接読み込み - Object.entries(dev_plugin_mapping).forEach(([key, plugin_module]) => { - // 例: key が "/src-tauri/plugins/sample/index.jsx" の場合、plugin_info.json のパスは同じディレクトリ内にある - const pluginInfoKey = key.replace("index.jsx", "plugin_info.json"); - const plugin_info = dev_plugin_info_mapping[pluginInfoKey]; - - if (!plugin_info) { - console.error("plugin_info.json has not found:", pluginInfoKey); + // `dev_plugins` を利用してプラグインを登録 + dev_plugins.forEach(({ index, plugin_info }) => { + if (!index || !plugin_info) { + console.error("Invalid development plugin detected", index, plugin_info); return; } - - // plugin_info を使ってプラグインコンテキストを生成 const plugin_context = generatePluginContext(plugin_info); - - if (plugin_module && plugin_module.init) { - plugin_module.init(plugin_context); + if (index.init) { + index.init(plugin_context); + } else { + console.error("Plugin missing init function", plugin_info); } }); } else { diff --git a/src-ui/plugins/index.js b/src-ui/plugins/index.js new file mode 100644 index 00000000..010c3ff0 --- /dev/null +++ b/src-ui/plugins/index.js @@ -0,0 +1,9 @@ +import plugin_index_1 from "./plugin_examples/index.jsx"; +import plugin_info_1 from "./plugin_examples/plugin_info.json"; + +export const dev_plugins = [ + { + index: { init: plugin_index_1 }, + plugin_info: plugin_info_1, + } +]; \ No newline at end of file diff --git a/src-tauri/plugins/plugin_examples/index.jsx b/src-ui/plugins/plugin_examples/index.jsx similarity index 100% rename from src-tauri/plugins/plugin_examples/index.jsx rename to src-ui/plugins/plugin_examples/index.jsx diff --git a/src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx b/src-ui/plugins/plugin_examples/main_container/MainContainer.jsx similarity index 100% rename from src-tauri/plugins/plugin_examples/main_container/MainContainer.jsx rename to src-ui/plugins/plugin_examples/main_container/MainContainer.jsx diff --git a/src-tauri/plugins/plugin_examples/plugin_info.json b/src-ui/plugins/plugin_examples/plugin_info.json similarity index 100% rename from src-tauri/plugins/plugin_examples/plugin_info.json rename to src-ui/plugins/plugin_examples/plugin_info.json diff --git a/src-tauri/plugins/plugin_examples/store/store.js b/src-ui/plugins/plugin_examples/store/store.js similarity index 100% rename from src-tauri/plugins/plugin_examples/store/store.js rename to src-ui/plugins/plugin_examples/store/store.js diff --git a/vite.config.js b/vite.config.js index 1abffdf1..58c824fa 100644 --- a/vite.config.js +++ b/vite.config.js @@ -33,11 +33,6 @@ export default defineConfig(async () => ({ resolve: { alias: { - "react": path.resolve(__dirname, "node_modules/react"), - "react-dom": path.resolve(__dirname, "node_modules/react-dom"), - - - "@root": path.resolve(__dirname), "@test_data": path.resolve(__dirname, "./test_data.js"), @@ -53,6 +48,8 @@ export default defineConfig(async () => ({ "@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"), + + "@dev_plugins_path": path.resolve(__dirname, "src-ui/plugins/index.js"), }, }, From 0c6829616c60df57df53169f212d016b38f07b91 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:46:43 +0900 Subject: [PATCH 16/47] [Update/bugfix] Plugins: Fix plugins position. Switchable rendering MessageContainer and Plugins. --- src-ui/app/main_page/main_section/MainSection.jsx | 14 ++++++++++---- .../main_page/main_section/MainSection.module.scss | 2 +- src-ui/app/main_page/main_section/PluginHost.jsx | 11 ++++------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src-ui/app/main_page/main_section/MainSection.jsx b/src-ui/app/main_page/main_section/MainSection.jsx index 2dec160c..d6b1f298 100644 --- a/src-ui/app/main_page/main_section/MainSection.jsx +++ b/src-ui/app/main_page/main_section/MainSection.jsx @@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next"; import styles from "./MainSection.module.scss"; import { TopBar } from "./top_bar/TopBar"; -// import { MessageContainer } from "./message_container/MessageContainer"; +import { MessageContainer } from "./message_container/MessageContainer"; import { LanguageSelector } from "./language_selector/LanguageSelector"; import { useStore_IsOpenedLanguageSelector } from "@store"; @@ -12,14 +12,20 @@ import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSys import { PluginHost } from "./PluginHost"; +import { usePlugins } from "@logics_configs"; + export const MainSection = () => { + const { currentPluginsData } = usePlugins(); + + const render_plugins = currentPluginsData.data.filter((plugin) => plugin.is_enabled && plugin.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..0f592a3a 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,7 @@ height: 100%; display: flex; flex-direction: column; - justify-content: space-between; + // justify-content: space-between; } .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 index 5c01328c..071ac934 100644 --- a/src-ui/app/main_page/main_section/PluginHost.jsx +++ b/src-ui/app/main_page/main_section/PluginHost.jsx @@ -1,17 +1,14 @@ import React from "react"; -import { usePlugins } from "@logics_configs"; -export const PluginHost = () => { - const { currentPluginsData } = usePlugins(); +export const PluginHost = ({render_components}) => { return ( -
- {currentPluginsData.data - .filter((plugin) => plugin.is_enabled && plugin.location === "main_section") + <> + {render_components .map((plugin, index) => { const PluginComponent = plugin.component; return PluginComponent ? : null; })} -
+ ); }; \ No newline at end of file From a59b9282df73346bfda02b0c00016921d2fc8fd6 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 27 Mar 2025 02:43:13 +0900 Subject: [PATCH 17/47] [Update] Plugins(VRCT Subtitles as testing one): Provide-able store and functions from main app. --- src-ui/app/App.jsx | 3 -- .../main_page/main_section/MainSection.jsx | 1 - src-ui/logics/configs/plugins/usePlugins.js | 4 +- src-ui/plugins/plugin_examples/index.jsx | 14 +++-- .../main_container/MainContainer.jsx | 22 -------- .../plugins/plugin_examples/plugin_info.json | 6 +-- src-ui/plugins/plugin_examples/store/store.js | 43 +++++++++++---- .../SubtitleSystemContainer.jsx | 0 .../SubtitleSystemContainer.module.scss | 0 .../_controllers/SubtitlesController.jsx | 7 ++- .../_logics/useSubtitles.jsx | 52 ++++++++++--------- .../_subtitles_utils.js | 0 .../CountdownContainer.jsx | 0 .../CountdownContainer.module.scss | 0 .../InputFileContainer.jsx | 0 .../InputFileContainer.module.scss | 0 .../ModeSelectorContainer.jsx | 0 .../ModeSelectorContainer.module.scss | 0 .../PlayControlContainer.jsx | 0 .../PlayControlContainer.module.scss | 0 .../SubtitlesListContainer.jsx | 0 .../SubtitlesListContainer.module.scss | 0 src-ui/store.js | 20 +------ 23 files changed, 85 insertions(+), 87 deletions(-) delete mode 100644 src-ui/plugins/plugin_examples/main_container/MainContainer.jsx rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/SubtitleSystemContainer.jsx (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/SubtitleSystemContainer.module.scss (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/_controllers/SubtitlesController.jsx (86%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/_logics/useSubtitles.jsx (82%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/_subtitles_utils.js (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/countdown_container/CountdownContainer.jsx (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/countdown_container/CountdownContainer.module.scss (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/input_file_container/InputFileContainer.jsx (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/input_file_container/InputFileContainer.module.scss (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/play_control_container/PlayControlContainer.jsx (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/play_control_container/PlayControlContainer.module.scss (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx (100%) rename src-ui/{app/main_page/main_section => plugins/plugin_examples}/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss (100%) diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index 269bf824..c102af71 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -23,8 +23,6 @@ import { SnackbarController } from "./snackbar_controller/SnackbarController"; import styles from "./App.module.scss"; import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common"; -import { SubtitlesController } from "./main_page/main_section/subtitle_system_container/_controllers/subtitlesController.jsx"; - export const App = () => { const { currentIsVrctAvailable } = useIsVrctAvailable(); const { currentIsBackendReady } = useIsBackendReady(); @@ -59,7 +57,6 @@ const Contents = ({ fetchPluginsHasRunRef }) => { return ( <> - {currentIsSoftwareUpdating.data === false diff --git a/src-ui/app/main_page/main_section/MainSection.jsx b/src-ui/app/main_page/main_section/MainSection.jsx index d6b1f298..0eaf272d 100644 --- a/src-ui/app/main_page/main_section/MainSection.jsx +++ b/src-ui/app/main_page/main_section/MainSection.jsx @@ -8,7 +8,6 @@ import { LanguageSelector } from "./language_selector/LanguageSelector"; import { useStore_IsOpenedLanguageSelector } from "@store"; import { useLanguageSettings } from "@logics_main"; import { useEffect } from "react"; -import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer"; import { PluginHost } from "./PluginHost"; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 2c337f7c..9a801674 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -16,6 +16,7 @@ import JSZip from "jszip"; import { useFetch } from "@logics_common"; import { useSoftwareVersion } from "@logics_configs"; +import * as logic_configs from "@logics_configs"; // PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL @@ -49,7 +50,8 @@ export const usePlugins = () => { : [...new_value, { plugin_id: plugin_info.plugin_id, location: plugin_info.location, component, is_downloaded: true }]; }); }, - createAtomWithHook: (...args) => createAtomWithHook(...args) + createAtomWithHook: (...args) => createAtomWithHook(...args), + logic_configs: logic_configs, }; return plugin_context; } diff --git a/src-ui/plugins/plugin_examples/index.jsx b/src-ui/plugins/plugin_examples/index.jsx index 4ded2716..011c133d 100644 --- a/src-ui/plugins/plugin_examples/index.jsx +++ b/src-ui/plugins/plugin_examples/index.jsx @@ -1,11 +1,19 @@ -import { initStore } from "./store/store"; -import { MainContainer } from "./main_container/MainContainer"; +import { initStore, StoreContext } from "./store/store.js"; +import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer"; +import { SubtitlesController } from "./subtitle_system_container/_controllers/subtitlesController.jsx"; export const init = (plugin_context) => { initStore(plugin_context.createAtomWithHook); + const { logic_configs } = plugin_context; const EntryComponents = () => { - return ; + return ( + + + + + + ); }; plugin_context.registerComponent(EntryComponents); diff --git a/src-ui/plugins/plugin_examples/main_container/MainContainer.jsx b/src-ui/plugins/plugin_examples/main_container/MainContainer.jsx deleted file mode 100644 index eaccc0aa..00000000 --- a/src-ui/plugins/plugin_examples/main_container/MainContainer.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useStore } from "../store/store"; -import { useEffect } from "react"; - -export const MainContainer = () => { - const { updateCountPluginState, currentCountPluginState } = useStore("useStore_CountPluginState"); - - const incrementCount = () => { - updateCountPluginState((prev_value) => ({ - count: prev_value.data.count + 1, - })); - }; - - useEffect(() => { - }, []) - - return ( -
-

1 Zipped Dev Plugin Count: {currentCountPluginState?.data?.count}

- -
- ); -}; \ No newline at end of file diff --git a/src-ui/plugins/plugin_examples/plugin_info.json b/src-ui/plugins/plugin_examples/plugin_info.json index 303f0ecb..f388a9cc 100644 --- a/src-ui/plugins/plugin_examples/plugin_info.json +++ b/src-ui/plugins/plugin_examples/plugin_info.json @@ -1,7 +1,7 @@ { - "title": "VRCT Example Plugins 1", - "plugin_id": "vrct_plugin_example_1", - "asset_name": "vrct_plugin_example_1.zip", + "title": "VRCT Subtitles", + "plugin_id": "vrct_plugin_subtitles", + "asset_name": "vrct_plugin_subtitles.zip", "location": "main_section", "plugin_version": "0.0.1", "min_supported_vrct_version": "3.0.4", diff --git a/src-ui/plugins/plugin_examples/store/store.js b/src-ui/plugins/plugin_examples/store/store.js index 9efd9dcf..40d80bb4 100644 --- a/src-ui/plugins/plugin_examples/store/store.js +++ b/src-ui/plugins/plugin_examples/store/store.js @@ -2,15 +2,30 @@ const store_hooks = {}; export const initStore = (createAtomWithHook) => { Object.assign(store_hooks, { - useStore_CountPluginState: createAtomWithHook( - { count: 10 }, - "CountPluginState" - ).useHook, + // useStore_CountPluginState: createAtomWithHook( + // { count: 10 }, + // "CountPluginState" + // ).useHook, - useStore_AnotherState: createAtomWithHook( - { value: "initial" }, - "AnotherState" - ).useHook, + // useStore_AnotherState: createAtomWithHook( + // { value: "initial" }, + // "AnotherState" + // ).useHook, + + 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, }); }; @@ -19,4 +34,14 @@ export const useStore = (hook_name) => { throw new Error(`Hook ${hook_name} is not initialized.`); } return store_hooks[hook_name](); -}; \ No newline at end of file +}; + + +// StoreContext.js +import React, { createContext, useContext } from "react"; + +export const StoreContext = createContext(null); + +export const useStoreContext = () => { + return useContext(StoreContext); +}; diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.jsx similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.jsx diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss b/src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.module.scss similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss rename to src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.module.scss diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/_controllers/SubtitlesController.jsx similarity index 86% rename from src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/_controllers/SubtitlesController.jsx index 720c3d28..53380205 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/_controllers/SubtitlesController.jsx +++ b/src-ui/plugins/plugin_examples/subtitle_system_container/_controllers/SubtitlesController.jsx @@ -1,9 +1,12 @@ -import { useSendTextToOverlay } from "@logics_configs"; +import { useStoreContext } from "../../store/store.js"; + import { useSubtitles } from "../_logics/useSubtitles"; import { secToDayTime } from "../_subtitles_utils" import { useEffect } from "react"; export const SubtitlesController = () => { - const { sendTextToOverlay } = useSendTextToOverlay(); + const logic_configs = useStoreContext(); + const { sendTextToOverlay } = logic_configs.useSendTextToOverlay(); + const { currentIsSubtitlePlaying, currentIsCuesScheduled, diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/_logics/useSubtitles.jsx similarity index 82% rename from src-ui/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/_logics/useSubtitles.jsx index 9ce1d626..fe0b6e3c 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/_logics/useSubtitles.jsx +++ b/src-ui/plugins/plugin_examples/subtitle_system_container/_logics/useSubtitles.jsx @@ -1,35 +1,39 @@ -import { useSendTextToOverlay } from "@logics_configs"; -import { - useStore_SubtitleFileName, - useStore_IsSubtitlePlaying, - useStore_SubtitlePlaybackMode, - useStore_SubtitleAbsoluteTargetTime, - useStore_IsCuesScheduled, - useStore_CountdownAdjustment, - useStore_EffectiveCountdown, - useStore_SubtitleCues, +import { useStore, useStoreContext } from "../../store/store.js"; - useStore_SubtitleTimers, - useStore_SubtitleCountdownTimerId, -} from "@store"; +// import { useSendTextToOverlay } from "@logics_configs"; +// import { +// useStore_SubtitleFileName, +// useStore_IsSubtitlePlaying, +// useStore_SubtitlePlaybackMode, +// useStore_SubtitleAbsoluteTargetTime, +// useStore_IsCuesScheduled, +// useStore_CountdownAdjustment, +// useStore_EffectiveCountdown, +// useStore_SubtitleCues, + +// useStore_SubtitleTimers, +// useStore_SubtitleCountdownTimerId, +// } from "../../store/store.js"; export const useSubtitles = () => { - const { sendTextToOverlay } = useSendTextToOverlay(); - const { currentSubtitleFileName, updateSubtitleFileName } = useStore_SubtitleFileName(); - const { currentIsSubtitlePlaying, updateIsSubtitlePlaying } = useStore_IsSubtitlePlaying(); - const { currentSubtitlePlaybackMode, updateSubtitlePlaybackMode } = useStore_SubtitlePlaybackMode(); - const { currentSubtitleAbsoluteTargetTime, updateSubtitleAbsoluteTargetTime } = useStore_SubtitleAbsoluteTargetTime(); - const { currentIsCuesScheduled, updateIsCuesScheduled } = useStore_IsCuesScheduled(); + const logic_configs = useStoreContext(); + const { sendTextToOverlay } = logic_configs.useSendTextToOverlay(); - const { currentCountdownAdjustment, updateCountdownAdjustment } = useStore_CountdownAdjustment(); - const { currentEffectiveCountdown, updateEffectiveCountdown } = useStore_EffectiveCountdown(); - const { currentSubtitleCues, updateSubtitleCues } = useStore_SubtitleCues(); + 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_SubtitleTimers(); + const { currentSubtitleTimers, updateSubtitleTimers, addSubtitleTimers } = useStore("useStore_SubtitleTimers"); // const timersRef = useRef([]); // カウントダウンタイマー専用の ref - const { currentSubtitleCountdownTimerId, updateSubtitleCountdownTimerId, AddSubtitleCountdownTimerId } = useStore_SubtitleCountdownTimerId(); + const { currentSubtitleCountdownTimerId, updateSubtitleCountdownTimerId, AddSubtitleCountdownTimerId } = useStore("useStore_SubtitleCountdownTimerId"); // cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる) const scheduleCues = (offset) => { diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/_subtitles_utils.js b/src-ui/plugins/plugin_examples/subtitle_system_container/_subtitles_utils.js similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/_subtitles_utils.js rename to src-ui/plugins/plugin_examples/subtitle_system_container/_subtitles_utils.js diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.jsx similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.jsx diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.module.scss b/src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.module.scss similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/countdown_container/CountdownContainer.module.scss rename to src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.module.scss diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.jsx similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.jsx diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.module.scss b/src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.module.scss similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/input_file_container/InputFileContainer.module.scss rename to src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.module.scss diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss b/src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss rename to src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.jsx similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.jsx diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.module.scss b/src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.module.scss similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/play_control_container/PlayControlContainer.module.scss rename to src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.module.scss diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx b/src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx rename to src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss b/src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss similarity index 100% rename from src-ui/app/main_page/main_section/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss rename to src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss diff --git a/src-ui/store.js b/src-ui/store.js index 410a3809..7e88792b 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -290,22 +290,4 @@ export const { atomInstance: Atom_IsOpenedTranslatorSelector, useHook: useStore_ export const { atomInstance: Atom_SupportersData, useHook: useStore_SupportersData } = createAtomWithHook(null, "SupportersData", {is_state_ok: true}); export const { atomInstance: Atom_VrctPosterIndex, useHook: useStore_VrctPosterIndex } = createAtomWithHook(0, "VrctPosterIndex"); -export const { atomInstance: Atom_PosterShowcaseWorldPageIndex, useHook: useStore_PosterShowcaseWorldPageIndex } = createAtomWithHook(0, "PosterShowcaseWorldPageIndex"); - -// ----------------------------------------------- -// Subtitles -// ----------------------------------------------- -export const { atomInstance: Atom_IsSubtitlePlaying, useHook: useStore_IsSubtitlePlaying } = createAtomWithHook(false, "IsSubtitlePlaying", { is_state_ok: true }); -export const { atomInstance: Atom_SubtitlePlaybackMode, useHook: useStore_SubtitlePlaybackMode } = createAtomWithHook("relative", "SubtitlePlaybackMode", { is_state_ok: true }); -export const { atomInstance: Atom_SubtitleAbsoluteTargetTime, useHook: useStore_SubtitleAbsoluteTargetTime } = createAtomWithHook({ - hour: "23", - minute: "00", -}, "SubtitleAbsoluteTargetTime", { is_state_ok: true }); -export const { atomInstance: Atom_IsCuesScheduled, useHook: useStore_IsCuesScheduled } = createAtomWithHook(false, "IsCuesScheduled", { is_state_ok: true }); -export const { atomInstance: Atom_CountdownAdjustment, useHook: useStore_CountdownAdjustment } = createAtomWithHook(0, "CountdownAdjustment", { is_state_ok: true }); -export const { atomInstance: Atom_EffectiveCountdown, useHook: useStore_EffectiveCountdown } = createAtomWithHook(null, "EffectiveCountdown", { is_state_ok: true }); -export const { atomInstance: Atom_SubtitleCues, useHook: useStore_SubtitleCues } = createAtomWithHook([], "SubtitleCues", { is_state_ok: true }); - -export const { atomInstance: Atom_SubtitleTimers, useHook: useStore_SubtitleTimers } = createAtomWithHook([], "SubtitleTimers", { is_state_ok: true }); -export const { atomInstance: Atom_SubtitleCountdownTimerId, useHook: useStore_SubtitleCountdownTimerId } = createAtomWithHook([], "SubtitleCountdownTimerId", { is_state_ok: true }); -export const { atomInstance: Atom_SubtitleFileName, useHook: useStore_SubtitleFileName } = createAtomWithHook("ファイルが選択されていません", "SubtitleFileName", { is_state_ok: true }); \ No newline at end of file +export const { atomInstance: Atom_PosterShowcaseWorldPageIndex, useHook: useStore_PosterShowcaseWorldPageIndex } = createAtomWithHook(0, "PosterShowcaseWorldPageIndex"); \ No newline at end of file From 5681038c22979106d44e26f833873bb859d29fc2 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 29 Mar 2025 16:22:00 +0900 Subject: [PATCH 18/47] [bugfix] Plugins(VRCT Subtitles as testing one): Apply styles by importing css file. --- .../_app_controllers/PluginsController.jsx | 2 ++ .../hotkeys_entry/HotkeysEntry.jsx | 2 +- .../setting_box/_components/slider/Slider.jsx | 2 +- .../SupportUsContainer.jsx | 2 +- .../setting_section/setting_box/vr/Vr.jsx | 2 +- .../version_label/VersionLabel.jsx | 2 +- .../SnackbarController.jsx | 2 +- .../common_components/checkbox/Checkbox.jsx | 2 +- src-ui/logics/configs/plugins/usePlugins.js | 22 +++++++++++++++++++ .../index.jsx | 19 ++++++++++++++-- .../plugin_info.json | 2 +- .../store/store.js | 0 .../SubtitleSystemContainer.jsx | 0 .../SubtitleSystemContainer.module.scss | 0 .../_controllers/SubtitlesController.jsx | 0 .../_logics/useSubtitles.jsx | 0 .../_subtitles_utils.js | 0 .../CountdownContainer.jsx | 0 .../CountdownContainer.module.scss | 0 .../InputFileContainer.jsx | 0 .../InputFileContainer.module.scss | 0 .../ModeSelectorContainer.jsx | 0 .../ModeSelectorContainer.module.scss | 0 .../PlayControlContainer.jsx | 2 +- .../PlayControlContainer.module.scss | 0 .../SubtitlesListContainer.jsx | 0 .../SubtitlesListContainer.module.scss | 0 src-ui/plugins/index.js | 4 ++-- 28 files changed, 52 insertions(+), 13 deletions(-) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/index.jsx (55%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/plugin_info.json (86%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/store/store.js (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/SubtitleSystemContainer.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/SubtitleSystemContainer.module.scss (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/_controllers/SubtitlesController.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/_logics/useSubtitles.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/_subtitles_utils.js (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/countdown_container/CountdownContainer.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/countdown_container/CountdownContainer.module.scss (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/input_file_container/InputFileContainer.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/input_file_container/InputFileContainer.module.scss (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/play_control_container/PlayControlContainer.jsx (94%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/play_control_container/PlayControlContainer.module.scss (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx (100%) rename src-ui/plugins/{plugin_examples => dev_plugin_subtitles}/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss (100%) diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index d6f97ab4..8689e2f0 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -1,8 +1,10 @@ import React, { useEffect, useRef } from "react"; import { usePlugins } from "@logics_configs"; +import clsx from "clsx"; if (typeof window !== "undefined") { window.React = React; + window.clsx = clsx; } export const PluginsController = ({ fetchPluginsHasRunRef }) => { 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/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/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/version_label/VersionLabel.jsx b/src-ui/app/config_page/version_label/VersionLabel.jsx index f6325165..be095e5c 100644 --- a/src-ui/app/config_page/version_label/VersionLabel.jsx +++ b/src-ui/app/config_page/version_label/VersionLabel.jsx @@ -1,6 +1,6 @@ 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"; 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/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/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 9a801674..a2b7857f 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -59,6 +59,7 @@ export const usePlugins = () => { const asyncLoadPlugin = async (plugin_folder_relative_path) => { const init_path = "plugins/" + plugin_folder_relative_path + "/index.esm.js"; const plugin_info_path = "plugins/" + plugin_folder_relative_path + "/plugin_info.json"; + const plugin_css_path = "plugins/" + plugin_folder_relative_path + "/main.css"; try { const plugin_info_json = await readTextFile(plugin_info_path, { dir: BaseDirectory.Resource, recursive: true }); const plugin_info = JSON.parse(plugin_info_json); @@ -80,6 +81,8 @@ export const usePlugins = () => { if (plugin_module && plugin_module.init) { plugin_module.init(generatePluginContext(plugin_info)); } + await loadPluginCSS(plugin_css_path); + } catch (error) { console.error("Failed to load plugin from", plugin_folder_relative_path, error); } @@ -290,3 +293,22 @@ const removeImportStatements = (code) => { .filter(line => !line.match(/^import\s+.*['"]react['"]/)) .join("\n"); }; + +// import { readTextFile, BaseDirectory } from "@tauri-apps/api/fs"; + +const loadPluginCSS = async (plugin_css_path) => { + 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/plugins/plugin_examples/index.jsx b/src-ui/plugins/dev_plugin_subtitles/index.jsx similarity index 55% rename from src-ui/plugins/plugin_examples/index.jsx rename to src-ui/plugins/dev_plugin_subtitles/index.jsx index 011c133d..2312d9d9 100644 --- a/src-ui/plugins/plugin_examples/index.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/index.jsx @@ -1,11 +1,13 @@ import { initStore, StoreContext } from "./store/store.js"; import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer"; -import { SubtitlesController } from "./subtitle_system_container/_controllers/subtitlesController.jsx"; +import { SubtitlesController } from "./subtitle_system_container/_controllers/SubtitlesController.jsx"; export const init = (plugin_context) => { initStore(plugin_context.createAtomWithHook); const { logic_configs } = plugin_context; + loadPluginCSS("./main.css"); + const EntryComponents = () => { return ( @@ -19,4 +21,17 @@ export const init = (plugin_context) => { plugin_context.registerComponent(EntryComponents); }; -export default init; \ No newline at end of file +export default init; + + +// CSS を動的に読み込む関数 +const loadPluginCSS = (cssUrl) => { + if (typeof document === "undefined") return; + // すでに読み込まれているかチェック + if (document.getElementById("plugin-main-css")) return; + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = cssUrl; + link.id = "plugin-main-css"; + document.head.appendChild(link); +}; \ No newline at end of file diff --git a/src-ui/plugins/plugin_examples/plugin_info.json b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json similarity index 86% rename from src-ui/plugins/plugin_examples/plugin_info.json rename to src-ui/plugins/dev_plugin_subtitles/plugin_info.json index f388a9cc..3bb37720 100644 --- a/src-ui/plugins/plugin_examples/plugin_info.json +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json @@ -3,7 +3,7 @@ "plugin_id": "vrct_plugin_subtitles", "asset_name": "vrct_plugin_subtitles.zip", "location": "main_section", - "plugin_version": "0.0.1", + "plugin_version": "0.0.0", "min_supported_vrct_version": "3.0.4", "max_supported_vrct_version": "3.0.6" } \ No newline at end of file diff --git a/src-ui/plugins/plugin_examples/store/store.js b/src-ui/plugins/dev_plugin_subtitles/store/store.js similarity index 100% rename from src-ui/plugins/plugin_examples/store/store.js rename to src-ui/plugins/dev_plugin_subtitles/store/store.js diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.module.scss similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/SubtitleSystemContainer.module.scss rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.module.scss diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/_controllers/SubtitlesController.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_controllers/SubtitlesController.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/_controllers/SubtitlesController.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_controllers/SubtitlesController.jsx diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/_logics/useSubtitles.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_logics/useSubtitles.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/_logics/useSubtitles.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_logics/useSubtitles.jsx diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/_subtitles_utils.js b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_subtitles_utils.js similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/_subtitles_utils.js rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/_subtitles_utils.js diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.jsx diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.module.scss b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.module.scss similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/countdown_container/CountdownContainer.module.scss rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/countdown_container/CountdownContainer.module.scss diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.jsx diff --git a/src-ui/plugins/plugin_examples/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 similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/input_file_container/InputFileContainer.module.scss rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/input_file_container/InputFileContainer.module.scss diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.jsx diff --git a/src-ui/plugins/plugin_examples/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 similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/mode_selector_container/ModeSelectorContainer.module.scss diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.jsx similarity index 94% rename from src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.jsx index 0e4fdbeb..05e45e01 100644 --- a/src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.jsx @@ -1,7 +1,7 @@ // import React, { useState, useRef, useEffect } from "react"; import styles from "./PlayControlContainer.module.scss"; import { useSubtitles } from "../_logics/useSubtitles"; -import { clsx } from "clsx"; +import clsx from "clsx"; export const PlayControlContainer = () => { const { diff --git a/src-ui/plugins/plugin_examples/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 similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/play_control_container/PlayControlContainer.module.scss rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/play_control_container/PlayControlContainer.module.scss diff --git a/src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.jsx diff --git a/src-ui/plugins/plugin_examples/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 similarity index 100% rename from src-ui/plugins/plugin_examples/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss rename to src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/subtitles_list_container/SubtitlesListContainer.module.scss diff --git a/src-ui/plugins/index.js b/src-ui/plugins/index.js index 010c3ff0..f92cb86b 100644 --- a/src-ui/plugins/index.js +++ b/src-ui/plugins/index.js @@ -1,5 +1,5 @@ -import plugin_index_1 from "./plugin_examples/index.jsx"; -import plugin_info_1 from "./plugin_examples/plugin_info.json"; +import plugin_index_1 from "./dev_plugin_subtitles/index.jsx"; +import plugin_info_1 from "./dev_plugin_subtitles/plugin_info.json"; export const dev_plugins = [ { From b0b68233aa391f7eb604a6e8e1471387d2ddbc9e Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 30 Mar 2025 04:18:54 +0900 Subject: [PATCH 19/47] [Update] Plugins: Send all logics to the plugins when it's registered. --- src-ui/logics/configs/plugins/usePlugins.js | 6 ++++-- src-ui/plugins/dev_plugin_subtitles/index.jsx | 21 +++---------------- .../dev_plugin_subtitles/store/store.js | 10 --------- .../_controllers/SubtitlesController.jsx | 4 ++-- .../_logics/useSubtitles.jsx | 19 ++--------------- 5 files changed, 11 insertions(+), 49 deletions(-) diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index a2b7857f..d03e88d0 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -16,7 +16,9 @@ import JSZip from "jszip"; import { useFetch } from "@logics_common"; import { useSoftwareVersion } from "@logics_configs"; -import * as logic_configs from "@logics_configs"; +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 @@ -51,7 +53,7 @@ export const usePlugins = () => { }); }, createAtomWithHook: (...args) => createAtomWithHook(...args), - logic_configs: logic_configs, + logics: { ...logics_common, ...logics_configs, ...logics_main } }; return plugin_context; } diff --git a/src-ui/plugins/dev_plugin_subtitles/index.jsx b/src-ui/plugins/dev_plugin_subtitles/index.jsx index 2312d9d9..56a3b91f 100644 --- a/src-ui/plugins/dev_plugin_subtitles/index.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/index.jsx @@ -4,13 +4,11 @@ import { SubtitlesController } from "./subtitle_system_container/_controllers/Su export const init = (plugin_context) => { initStore(plugin_context.createAtomWithHook); - const { logic_configs } = plugin_context; - - loadPluginCSS("./main.css"); + const { logics } = plugin_context; const EntryComponents = () => { return ( - + @@ -21,17 +19,4 @@ export const init = (plugin_context) => { plugin_context.registerComponent(EntryComponents); }; -export default init; - - -// CSS を動的に読み込む関数 -const loadPluginCSS = (cssUrl) => { - if (typeof document === "undefined") return; - // すでに読み込まれているかチェック - if (document.getElementById("plugin-main-css")) return; - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.href = cssUrl; - link.id = "plugin-main-css"; - document.head.appendChild(link); -}; \ No newline at end of file +export default init; \ 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 index 40d80bb4..6f338573 100644 --- a/src-ui/plugins/dev_plugin_subtitles/store/store.js +++ b/src-ui/plugins/dev_plugin_subtitles/store/store.js @@ -2,16 +2,6 @@ const store_hooks = {}; export const initStore = (createAtomWithHook) => { Object.assign(store_hooks, { - // useStore_CountPluginState: createAtomWithHook( - // { count: 10 }, - // "CountPluginState" - // ).useHook, - - // useStore_AnotherState: createAtomWithHook( - // { value: "initial" }, - // "AnotherState" - // ).useHook, - useStore_IsSubtitlePlaying: createAtomWithHook(false, "IsSubtitlePlaying", { is_state_ok: true }).useHook, useStore_SubtitlePlaybackMode: createAtomWithHook("relative", "SubtitlePlaybackMode", { is_state_ok: true }).useHook, useStore_SubtitleAbsoluteTargetTime: createAtomWithHook({ 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 index 53380205..d58be5aa 100644 --- 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 @@ -4,8 +4,8 @@ import { useSubtitles } from "../_logics/useSubtitles"; import { secToDayTime } from "../_subtitles_utils" import { useEffect } from "react"; export const SubtitlesController = () => { - const logic_configs = useStoreContext(); - const { sendTextToOverlay } = logic_configs.useSendTextToOverlay(); + const { useSendTextToOverlay } = useStoreContext(); + const { sendTextToOverlay } = useSendTextToOverlay(); const { currentIsSubtitlePlaying, 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 index fe0b6e3c..80585abf 100644 --- 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 @@ -1,23 +1,8 @@ import { useStore, useStoreContext } from "../../store/store.js"; -// import { useSendTextToOverlay } from "@logics_configs"; -// import { -// useStore_SubtitleFileName, -// useStore_IsSubtitlePlaying, -// useStore_SubtitlePlaybackMode, -// useStore_SubtitleAbsoluteTargetTime, -// useStore_IsCuesScheduled, -// useStore_CountdownAdjustment, -// useStore_EffectiveCountdown, -// useStore_SubtitleCues, - -// useStore_SubtitleTimers, -// useStore_SubtitleCountdownTimerId, -// } from "../../store/store.js"; - export const useSubtitles = () => { - const logic_configs = useStoreContext(); - const { sendTextToOverlay } = logic_configs.useSendTextToOverlay(); + const { useSendTextToOverlay } = useStoreContext(); + const { sendTextToOverlay } = useSendTextToOverlay(); const { currentSubtitleFileName, updateSubtitleFileName } = useStore("useStore_SubtitleFileName"); const { currentIsSubtitlePlaying, updateIsSubtitlePlaying } = useStore("useStore_IsSubtitlePlaying"); From c02d7c49e901b8201d2c95d05baadca9825f103d Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 1 Apr 2025 06:02:58 +0900 Subject: [PATCH 20/47] [Update] Plugins dev: Apply-able aliases. --- src-ui/logics/configs/plugins/usePlugins.js | 15 +- src-ui/plugins/dev_plugin_subtitles/index.jsx | 2 +- .../dev_plugin_subtitles/plugin_configs.js | 5 + src-ui/plugins/index.js | 9 -- src-ui/plugins/plugins_index.js | 3 + vite.config.js | 130 +++++++++++------- 6 files changed, 102 insertions(+), 62 deletions(-) create mode 100644 src-ui/plugins/dev_plugin_subtitles/plugin_configs.js delete mode 100644 src-ui/plugins/index.js create mode 100644 src-ui/plugins/plugins_index.js diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index d03e88d0..9031bc57 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -10,12 +10,20 @@ 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 "@dev_plugins_path"; +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`), + plugin_info: await import(`@plugins_path/${entry_path}/plugin_info.json`), + }); +}) import JSZip from "jszip"; import { useFetch } from "@logics_common"; import { useSoftwareVersion } from "@logics_configs"; + import * as logics_configs from "@logics_configs"; import * as logics_main from "@logics_main"; import * as logics_common from "@logics_common"; @@ -91,9 +99,8 @@ export const usePlugins = () => { }; const asyncLoadAllPlugins = async () => { - if (import.meta.env.DEV) { - // `dev_plugins` を利用してプラグインを登録 - dev_plugins.forEach(({ index, plugin_info }) => { + if (!import.meta.env.DEV) { + imported_dev_plugins.forEach(({ index, plugin_info }) => { if (!index || !plugin_info) { console.error("Invalid development plugin detected", index, plugin_info); return; diff --git a/src-ui/plugins/dev_plugin_subtitles/index.jsx b/src-ui/plugins/dev_plugin_subtitles/index.jsx index 56a3b91f..1b8942d4 100644 --- a/src-ui/plugins/dev_plugin_subtitles/index.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/index.jsx @@ -1,4 +1,4 @@ -import { initStore, StoreContext } from "./store/store.js"; +import { initStore, StoreContext } from "@plugin_store"; import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer"; import { SubtitlesController } from "./subtitle_system_container/_controllers/SubtitlesController.jsx"; 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..0944d0b6 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_configs.js @@ -0,0 +1,5 @@ +export const configs = { + alias: { + "@plugin_store": "store/store.js", + } +} \ No newline at end of file diff --git a/src-ui/plugins/index.js b/src-ui/plugins/index.js deleted file mode 100644 index f92cb86b..00000000 --- a/src-ui/plugins/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import plugin_index_1 from "./dev_plugin_subtitles/index.jsx"; -import plugin_info_1 from "./dev_plugin_subtitles/plugin_info.json"; - -export const dev_plugins = [ - { - index: { init: plugin_index_1 }, - plugin_info: plugin_info_1, - } -]; \ No newline at end of file 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/vite.config.js b/vite.config.js index 58c824fa..948083e4 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,62 +3,96 @@ import react from "@vitejs/plugin-react"; import svgr from "vite-plugin-svgr"; 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: [react(), svgr()], + assetsInclude: ["**/*.yml"], - 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"), - - "@dev_plugins_path": path.resolve(__dirname, "src-ui/plugins/index.js"), + build: { + outDir: path.resolve(__dirname, "dist"), + rollupOptions: { + input: { + main: path.resolve(__dirname, "index.html"), + }, + }, }, - }, - 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 From 67f32ad7b9025960d52c62a0bd56a05f1f398850 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 2 Apr 2025 22:52:20 +0900 Subject: [PATCH 21/47] [Update] Plugins: Add update function. --- .../_app_controllers/PluginsController.jsx | 38 ++++++++++---- .../_download_button/_DownloadButton.jsx | 9 ++++ .../_DownloadButton.module.scss | 14 +++++ .../PluginsControlComponent.jsx | 51 +++++++------------ .../PluginsControlComponent.module.scss | 4 ++ .../setting_box/plugins/Plugins.jsx | 3 +- src-ui/logics/configs/plugins/usePlugins.js | 16 +++--- 7 files changed, 83 insertions(+), 52 deletions(-) diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 8689e2f0..ec181bb9 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -23,25 +23,43 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { const info_array = await asyncFetchPluginsInfo(); updatePluginsData(prev => { // Map を利用してそれぞれの配列を plugin_id で参照できるようにする - const infoMap = new Map(info_array.map(info => [info.plugin_id, info])); - const prevMap = new Map(prev.data.map(item => [item.plugin_id, item])); + const info_map = new Map(info_array.map(info => [info.plugin_id, info])); + const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); // info_array にある各アイテムについて、prev.data に同じ plugin_id があればマージ const merged = info_array.map(info => { - if (prevMap.has(info.plugin_id)) { - return { ...info, ...prevMap.get(info.plugin_id) }; + if (prev_map.has(info.plugin_id)) { + return { + ...info, + latest_plugin_version: info.plugin_version, + ...prev_map.get(info.plugin_id), + }; } - return info; + return { + ...info, + latest_plugin_version: info.plugin_version, + }; }); - // prev.data にのみ存在するアイテムを追加し、is_outdated: true を付与 + // prev.data にのみ存在するアイテム = latest plugin infoには存在しない + // を追加し、is_outdated: true を付与 prev.data.forEach(item => { - if (!infoMap.has(item.plugin_id)) { + if (!info_map.has(item.plugin_id)) { merged.push({ ...item, is_outdated: true }); } }); - return merged; + let new_value = []; + for (const plugin of merged) { + if (plugin.downloaded_plugin_version !== plugin.latest_plugin_version && plugin.is_plugin_supported) { + plugin.is_latest_version_available = true; + } else { + plugin.is_latest_version_available = false; + } + new_value.push(plugin); + } + + return new_value; }); } catch (error) { console.error(error); @@ -59,7 +77,7 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { updatePluginsData(prev => { // currentSavedPluginsStatus.data の各要素を Map 化して plugin_id で参照 const savedMap = new Map(currentSavedPluginsStatus.data.map(saved => [saved.plugin_id, saved])); - const prevMap = new Map(prev.data.map(item => [item.plugin_id, item])); + const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); // prev.data にある各アイテムについて、保存済みの状態情報があればマージ const merged = prev.data.map(item => { @@ -71,7 +89,7 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { // currentSavedPluginsStatus.data にのみ存在する項目があれば追加 currentSavedPluginsStatus.data.forEach(saved => { - if (!prevMap.has(saved.plugin_id)) { + if (!prev_map.has(saved.plugin_id)) { merged.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); } }); 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 index 41c3b7d9..edaf7fa5 100644 --- 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 @@ -32,6 +32,15 @@ export const _DownloadButton = ({option, ...props}) => {

{t("config_page.model_download_button_label")}

); + case option.update_button: + return ( + + ); default: return null; } 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 index 6b42ca5d..752dca38 100644 --- 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 @@ -28,4 +28,18 @@ .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/plugins_control_component/PluginsControlComponent.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/plugins_control_component/PluginsControlComponent.jsx index 33ca0fa7..a8f70b73 100644 --- 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 @@ -1,48 +1,31 @@ -import { - SwitchBox, -} from "../index"; +import { SwitchBox } from "../index"; import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; import styles from "./PluginsControlComponent.module.scss"; -export const PluginsControlComponent = ({ variable_state, plugin_status, toggleFunction, ...props }) => { +export const PluginsControlComponent = ({ variable_state, plugin_status, toggleFunction, downloadStartFunction }) => { + const { plugin_id, is_pending, is_downloaded, is_enabled, is_latest_version_available, is_plugin_supported, is_outdated } = plugin_status; + const option = { - id: plugin_status.plugin_id, - is_pending: plugin_status.is_pending, - is_downloaded: plugin_status.is_downloaded, - data: plugin_status.is_enabled, + id: plugin_id, + is_pending: is_pending, + is_downloaded: is_downloaded, + data: is_enabled, + update_button: is_downloaded && is_latest_version_available, state: variable_state, progress: null, }; - const adjustedToggleFunction = () => { - toggleFunction(plugin_status.plugin_id); - }; - - let is_turn_on_able = false; - if (plugin_status.is_downloaded && plugin_status.is_plugin_supported) { - is_turn_on_able = true; - } - if (plugin_status.is_outdated) { - is_turn_on_able = true; - } + const togglePlugin = () => toggleFunction(plugin_id); + const is_turn_on_able = is_downloaded && (is_plugin_supported || is_outdated); return (
- {is_turn_on_able && - } - {plugin_status.is_plugin_supported ? - <_DownloadButton - option={option} - downloadStartFunction={props.downloadStartFunction} - /> - : -
- Downloaded but outdated. -
- } + {is_plugin_supported ? ( + <_DownloadButton option={option} downloadStartFunction={downloadStartFunction} /> + ) : ( +
Downloaded but outdated.
+ )} + {is_turn_on_able && }
); }; \ No newline at end of file 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 index 99a37516..816119fc 100644 --- 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 @@ -1,4 +1,8 @@ .container { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; } .unavailable_text { diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index cddda330..4c54df95 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -88,7 +88,8 @@ const PluginDownloadContainer = () => { ) : (
-

Version: {plugin.plugin_version}

+

Downloaded Version: {plugin.downloaded_plugin_version}

+

Latest Version: {plugin.latest_plugin_version}

Compatible: {plugin.min_supported_vrct_version} ~ {plugin.max_supported_vrct_version}

diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 9031bc57..22137654 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -47,17 +47,19 @@ export const usePlugins = () => { return console.error("An invalid plugin was detected.", plugin_info.plugin_id, plugin_info.location, component); } updatePluginsData(prev => { - const is_already_registered = prev.data.some(old_value => old_value.plugin_id === plugin_info.plugin_id); - const new_value = prev.data.map(old_value => old_value.plugin_id === plugin_info.plugin_id - ? { ...old_value, ...plugin_info, location: plugin_info.location, component, is_downloaded: true } - : old_value + ? { + ...old_value, + ...plugin_info, + downloaded_plugin_version: plugin_info.plugin_version, + component: component, + is_downloaded: true, + is_latest_version_available: false, + } : old_value ); - return is_already_registered - ? new_value - : [...new_value, { plugin_id: plugin_info.plugin_id, location: plugin_info.location, component, is_downloaded: true }]; + return new_value; }); }, createAtomWithHook: (...args) => createAtomWithHook(...args), From 7e637b795d627300aafbb6b7b7f0a409d6f419da Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:29:31 +0900 Subject: [PATCH 22/47] [Update/Refactor/bugfix] Update: Add functions and test ui. Update, backend: send latest vrct version to frontend. Refactor: Change the plugins data structure. bugfix: fix endless showing update button. --- src-python/controller.py | 6 +- src-python/mainloop.py | 2 +- src-python/model.py | 6 +- .../_app_controllers/PluginsController.jsx | 72 +++++---- .../PluginsControlComponent.jsx | 145 +++++++++++++++--- .../setting_box/plugins/Plugins.jsx | 81 +++++----- .../version_label/VersionLabel.jsx | 3 +- .../main_page/main_section/MainSection.jsx | 2 +- .../RightSideComponents.jsx | 6 +- .../update_modal/UpdateModal.jsx | 35 ++++- src-ui/logics/common/index.js | 2 +- .../common/useIsSoftwareUpdateAvailable.js | 10 -- src-ui/logics/common/useSoftwareVersion.js | 40 +++++ src-ui/logics/configs/index.js | 4 +- src-ui/logics/configs/plugins/usePlugins.js | 138 ++++++++++++----- src-ui/logics/configs/useSoftwareVersion.js | 18 --- src-ui/logics/useReceiveRoutes.js | 7 +- src-ui/store.js | 7 +- 18 files changed, 404 insertions(+), 180 deletions(-) delete mode 100644 src-ui/logics/common/useIsSoftwareUpdateAvailable.js create mode 100644 src-ui/logics/common/useSoftwareVersion.js delete mode 100644 src-ui/logics/configs/useSoftwareVersion.js diff --git a/src-python/controller.py b/src-python/controller.py index 02ba5e34..faa659b2 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 diff --git a/src-python/mainloop.py b/src-python/mainloop.py index edd5356a..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", diff --git a/src-python/model.py b/src-python/model.py index f393314d..afe0d565 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-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index ec181bb9..2fad4376 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -14,6 +14,7 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { currentPluginsData, updatePluginsData, currentSavedPluginsStatus, + updateIsPluginsInitialized, } = usePlugins(); useEffect(() => { @@ -26,40 +27,55 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { const info_map = new Map(info_array.map(info => [info.plugin_id, info])); const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); - // info_array にある各アイテムについて、prev.data に同じ plugin_id があればマージ - const merged = info_array.map(info => { - if (prev_map.has(info.plugin_id)) { - return { - ...info, - latest_plugin_version: info.plugin_version, - ...prev_map.get(info.plugin_id), + const new_data = []; + for (const info of info_array) { + let new_plugin_info = {}; + if (prev_map.has(info.plugin_id)) { // plugin_id 登録済み + const target_downloaded_plugin = prev_map.get(info.plugin_id); + if (target_downloaded_plugin.is_downloaded) { // 既にダウンロード済み + const is_latest_version_available = !(target_downloaded_plugin.plugin_version === info.plugin_version); + + new_plugin_info = { + is_downloaded: true, + is_latest_version_already: (target_downloaded_plugin.downloaded_plugin_info?.plugin_version === info.plugin_version), + is_latest_version_available: is_latest_version_available, + latest_plugin_info: { ...info }, + ...target_downloaded_plugin, + }; + } else { // infoにもあり登録済みだがダウンロードされていない + new_plugin_info = { + is_downloaded: false, + is_latest_version_already: false, + is_latest_version_available: info.is_latest_version_available, + latest_plugin_info: { ...info }, + ...target_downloaded_plugin, + } + } + } else { // 未ダウンロード + new_plugin_info = { + plugin_id: info.plugin_id, + is_downloaded: false, + is_latest_version_already: false, + latest_plugin_info: { ...info }, }; } - return { - ...info, - latest_plugin_version: info.plugin_version, - }; - }); + new_data.push(new_plugin_info); + } // prev.data にのみ存在するアイテム = latest plugin infoには存在しない // を追加し、is_outdated: true を付与 prev.data.forEach(item => { if (!info_map.has(item.plugin_id)) { - merged.push({ ...item, is_outdated: true }); + new_data.push({ ...item, is_outdated: true }); } }); - let new_value = []; - for (const plugin of merged) { - if (plugin.downloaded_plugin_version !== plugin.latest_plugin_version && plugin.is_plugin_supported) { - plugin.is_latest_version_available = true; - } else { - plugin.is_latest_version_available = false; + new_data.forEach(plugin => { + if (!plugin.is_outdated) { + plugin.is_latest_version_available = (plugin.latest_plugin_info.is_plugin_supported); } - new_value.push(plugin); - } - - return new_value; + }); + return new_data; }); } catch (error) { console.error(error); @@ -68,6 +84,7 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { if (!fetchPluginsHasRunRef.current) { loadPlugins(); + updateIsPluginsInitialized(true); } return () => fetchPluginsHasRunRef.current = true; }, []); @@ -76,13 +93,13 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { useEffect(() => { updatePluginsData(prev => { // currentSavedPluginsStatus.data の各要素を Map 化して plugin_id で参照 - const savedMap = new Map(currentSavedPluginsStatus.data.map(saved => [saved.plugin_id, saved])); + 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])); - // prev.data にある各アイテムについて、保存済みの状態情報があればマージ const merged = prev.data.map(item => { - if (savedMap.has(item.plugin_id)) { - return { ...item, is_enabled: savedMap.get(item.plugin_id).is_enabled }; + + if (saved_map.has(item.plugin_id)) { + return { ...item, is_enabled: saved_map.get(item.plugin_id).is_enabled }; } return item; }); @@ -93,7 +110,6 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { merged.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); } }); - return merged; }); }, [currentSavedPluginsStatus]); 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 index a8f70b73..0fd7c33a 100644 --- 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 @@ -1,31 +1,136 @@ +import React from "react"; import { SwitchBox } from "../index"; import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; import styles from "./PluginsControlComponent.module.scss"; -export const PluginsControlComponent = ({ variable_state, plugin_status, toggleFunction, downloadStartFunction }) => { - const { plugin_id, is_pending, is_downloaded, is_enabled, is_latest_version_available, is_plugin_supported, is_outdated } = plugin_status; - +// メインのコントロールコンポーネント。ダウンロード済み / 未ダウンロードで分岐して表示する +export const PluginsControlComponent = ({ + variable_state, + plugin_status, + toggleFunction, + downloadStartFunction, +}) => { + // 共通オプション(各子コンポーネントに引き回す情報) const option = { - id: plugin_id, - is_pending: is_pending, - is_downloaded: is_downloaded, - data: is_enabled, - update_button: is_downloaded && is_latest_version_available, + 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 togglePlugin = () => toggleFunction(plugin_id); - const is_turn_on_able = is_downloaded && (is_plugin_supported || is_outdated); + if (plugin_status.is_downloaded) { + return ( + + ); + } else { + return ( + + ); + } +}; - return ( -
- {is_plugin_supported ? ( +// ------------------------- +// ダウンロード済みのプラグイン用コンポーネント +// 状態により以下の分岐を行う +// ・ is_latest_version_already が true なら「最新版を使用中」 +// ・ is_latest_version_already が false かつ is_latest_version_available が true なら「最新版を利用可能」(アップデートボタン+スイッチ) +// ・ それ以外(is_latest_version_already:false && is_latest_version_available: false)なら、desc等の情報とスイッチのみ表示 +const DownloadedPluginControl = ({ + option, + plugin_status, + toggleFunction, + downloadStartFunction, +}) => { + // on/off トグル時の処理 + const togglePlugin = () => { + toggleFunction(plugin_status.plugin_id); + }; + + + // ダウンロード済みの場合、ダウンロードされた情報からタイトルやバージョンを取得 + const title = plugin_status.downloaded_plugin_info?.title || plugin_status.latest_plugin_info.title; + const current_version = + plugin_status.downloaded_plugin_info?.plugin_version || plugin_status.latest_plugin_info.plugin_version; + + // コンポーネントごとに表示内容を分岐 + if (plugin_status.is_latest_version_already) { + // 最新版が既に使用中 + return ( +
+

{title}

+

現在のバージョン: {current_version}

+

最新版を使用中

+ +
+ ); + } else if (plugin_status.is_latest_version_available) { + // 最新版の利用可能なお知らせとアップデートボタン+スイッチ + return ( +
+

{title}

+

現在のバージョン: {current_version}

+

最新版を利用可能

<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} /> - ) : ( -
Downloaded but outdated.
- )} - {is_turn_on_able && } -
- ); -}; \ No newline at end of file + +
+ ); + } else { + // 最新版利用可能ではないがダウンロード済み + // ※「desc」に関しては、情報があればplugin_status.descやlatest_plugin_info.descを利用してください + const desc = + plugin_status.latest_plugin_info.desc || + "追加情報がありません。"; + return ( +
+

{title}

+

現在のバージョン: {current_version}

+

{desc}

+ +
+ ); + } +}; + +// ------------------------- +// 未ダウンロードのプラグイン用コンポーネント +// 状態により以下の分岐を行う +// ・ is_latest_version_available が true なら:info_title, 最新バージョン情報とダウンロードボタン +// ・ is_latest_version_available が false なら:info_title, 最新バージョン情報と「現在利用不可」 +const NotDownloadedPluginControl = ({ option, plugin_status, downloadStartFunction }) => { + const title = plugin_status.latest_plugin_info.title; + const latest_version = plugin_status.latest_plugin_info.plugin_version; + // ※未ダウンロードの場合、current_versionは「未ダウンロード」や「なし」といった扱いとする + const current_version = "未ダウンロード"; + + if (plugin_status.is_latest_version_available) { + return ( +
+

{title}

+

現在のバージョン: {current_version}

+

最新バージョン: {latest_version}

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

{title}

+

現在のバージョン: {current_version}

+

最新バージョン: {latest_version}

+

現在利用不可

+
+ ); + } +}; diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 4c54df95..2a3fd42d 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -1,9 +1,15 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import { usePlugins } from "@logics_configs"; import styles from "./Plugins.module.scss"; -import { PluginsControlComponent } from "../_components"; +import { PluginsControlComponent } from "../_components/plugins_control_component/PluginsControlComponent"; export const Plugins = () => { + const { + currentIsPluginsInitialized, + } = usePlugins(); + + if (!currentIsPluginsInitialized.data) return null; + return (
@@ -11,6 +17,7 @@ export const Plugins = () => { ); }; + const PluginDownloadContainer = () => { const { downloadAndExtractPlugin, @@ -18,40 +25,29 @@ const PluginDownloadContainer = () => { updatePluginsData, currentSavedPluginsStatus, setSavedPluginsStatus, + handlePendingPlugin, } = usePlugins(); - + // ダウンロード開始時の状態更新処理 const downloadStartFunction = async (target_plugin_id) => { - updatePluginsData((old_value) => { - const new_value = old_value.data.map(d => { - if (d.plugin_id === target_plugin_id) { - d.is_pending = true; - } - return d; - }); - return new_value; - }); - const target_plugin_info = currentPluginsData.data.find(d => d.plugin_id === target_plugin_id); + handlePendingPlugin(target_plugin_id, true); + + const target_plugin_info = currentPluginsData.data.find( + (d) => d.plugin_id === target_plugin_id + ); downloadAndExtractPlugin(target_plugin_info).then(() => { - updatePluginsData((old_value) => { - const new_value = old_value.data.map(d => { - if (d.plugin_id === target_plugin_id) { - d.is_pending = false; - d.is_downloaded = true; - } - return d; - }); - return new_value; - }); - }) + handlePendingPlugin(target_plugin_id, false); + }); }; - + // プラグインのオンオフ切り替え処理 const toggleFunction = (target_plugin_id) => { - const is_exists = currentSavedPluginsStatus.data.some(d => d.plugin_id === 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 => { + new_value = currentSavedPluginsStatus.data.map((d) => { if (d.plugin_id === target_plugin_id) { d.is_enabled = !d.is_enabled; } @@ -65,40 +61,45 @@ const PluginDownloadContainer = () => { }); } - - // currentPluginsData.data で、is_downloaded が true のものだけ残す - new_value = new_value.filter(item => { - return currentPluginsData.data.some(plugin => plugin.plugin_id === item.plugin_id && plugin.is_downloaded); - }); + // 「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); - } + }; const variable_state = currentSavedPluginsStatus.state; - return (
{currentPluginsData.data.map((plugin) => (
-

{plugin.title}

+

+ {plugin.is_downloaded + ? plugin.downloaded_plugin_info?.title || plugin.latest_plugin_info.title + : plugin.latest_plugin_info.title} +

{plugin.plugin_id}

{plugin.error ? (

Error: {plugin.error}

) : (
-

Downloaded Version: {plugin.downloaded_plugin_version}

-

Latest Version: {plugin.latest_plugin_version}

+ {/* 状態に応じた情報表示(例:バージョン等) */}

- Compatible: {plugin.min_supported_vrct_version} ~ {plugin.max_supported_vrct_version} + {plugin.is_downloaded + ? `現在のバージョン: ${plugin.downloaded_plugin_info?.plugin_version}` + : `最新バージョン: ${plugin.latest_plugin_info.plugin_version}`}

)} @@ -106,4 +107,4 @@ const PluginDownloadContainer = () => { ))}
); -}; \ No newline at end of file +}; diff --git a/src-ui/app/config_page/version_label/VersionLabel.jsx b/src-ui/app/config_page/version_label/VersionLabel.jsx index be095e5c..95abd94d 100644 --- a/src-ui/app/config_page/version_label/VersionLabel.jsx +++ b/src-ui/app/config_page/version_label/VersionLabel.jsx @@ -3,8 +3,7 @@ import { useState } from "react"; 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/main_page/main_section/MainSection.jsx b/src-ui/app/main_page/main_section/MainSection.jsx index 0eaf272d..e89071a8 100644 --- a/src-ui/app/main_page/main_section/MainSection.jsx +++ b/src-ui/app/main_page/main_section/MainSection.jsx @@ -16,7 +16,7 @@ import { usePlugins } from "@logics_configs"; export const MainSection = () => { const { currentPluginsData } = usePlugins(); - const render_plugins = currentPluginsData.data.filter((plugin) => plugin.is_enabled && plugin.location === "main_section"); + const render_plugins = currentPluginsData.data.filter((plugin) => (plugin.is_downloaded && plugin.is_enabled && plugin.downloaded_plugin_info.location === "main_section")); return (
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..4fef1c71 100644 --- a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx +++ b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx @@ -1,8 +1,14 @@ 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 { usePlugins } from "@logics_configs"; +import { + useComputeMode, + useUpdateSoftware, + useIsSoftwareUpdating, + useSoftwareVersion, +} from "@logics_common"; + import clsx from "clsx"; export const UpdateModal = () => { @@ -11,9 +17,13 @@ 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; + console.log(isAnyPluginEnabled()); + + + const is_latest_version_already = currentLatestSoftwareVersionInfo.data.is_update_available === false; const is_cpu_version = currentComputeMode.data === "cpu"; const onClickUpdateSoftware = () => { @@ -37,6 +47,7 @@ export const UpdateModal = () => { return (
+ {isAnyPluginEnabled() && }
@@ -85,4 +96,20 @@ const CurrentVersionLabel = (props) => { return

{t("update_modal.is_latest_version_already")}

; } return

{t("update_modal.is_current_compute_device")}

; +}; + +const PluginUpdateNotification = () => { + const { enabledPluginsList } = usePlugins(); + + const incompatible_plugins_list = enabledPluginsList(); + + return ( +
+ {incompatible_plugins_list.map(plugin => { + console.log(plugin); + + return

{plugin.title}

+ })} +
+ ); }; \ No newline at end of file diff --git a/src-ui/logics/common/index.js b/src-ui/logics/common/index.js index 5feee60e..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"; 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 85d7c553..912e2a7d 100644 --- a/src-ui/logics/configs/index.js +++ b/src-ui/logics/configs/index.js @@ -62,6 +62,4 @@ 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 index 22137654..d4910f46 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,10 +1,9 @@ -import semver from "semver"; - import { invoke } from "@tauri-apps/api/tauri"; import { createAtomWithHook, useStore_SavedPluginsStatus, useStore_PluginsData, + useStore_IsPluginsInitialized, } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; @@ -21,8 +20,7 @@ dev_plugins.forEach(async ({entry_path}) => { import JSZip from "jszip"; -import { useFetch } from "@logics_common"; -import { useSoftwareVersion } from "@logics_configs"; +import { useFetch, useSoftwareVersion } from "@logics_common"; import * as logics_configs from "@logics_configs"; import * as logics_main from "@logics_main"; @@ -36,30 +34,66 @@ export const usePlugins = () => { const { asyncStdoutToPython } = useStdoutToPython(); const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); - const { currentSoftwareVersion } = useSoftwareVersion(); + const { currentIsPluginsInitialized, updateIsPluginsInitialized, pendingIsPluginsInitialized } = useStore_IsPluginsInitialized(); + const { checkVrctVerCompatibility } = useSoftwareVersion(); const { asyncTauriFetchGithub } = useFetch(); - const generatePluginContext= (plugin_info) => { + const generatePluginContext= (downloaded_plugin_info) => { const plugin_context = { registerComponent: (component) => { - if (!plugin_info.plugin_id || !plugin_info.location || !component) { - return console.error("An invalid plugin was detected.", plugin_info.plugin_id, plugin_info.location, 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); } updatePluginsData(prev => { - const new_value = prev.data.map(old_value => - old_value.plugin_id === plugin_info.plugin_id - ? { - ...old_value, - ...plugin_info, - downloaded_plugin_version: plugin_info.plugin_version, + const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); + const new_data = []; + let new_value = {}; + + if (!prev_map.has(downloaded_plugin_info.plugin_id)) { // 未ダウンロード 新規登録 + new_value = { + plugin_id: downloaded_plugin_info.plugin_id, + component: component, + is_downloaded: true, + is_latest_version_available: false, + downloaded_plugin_info: { + ...downloaded_plugin_info, + component: component, + is_plugin_supported: is_plugin_supported, + is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, + }, + }; + return [...prev.data, new_value]; + } + + for (const old_plugin_data of prev.data) { + const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(downloaded_plugin_info.min_supported_vrct_version, downloaded_plugin_info.max_supported_vrct_version); + + if (prev_map.has(downloaded_plugin_info.plugin_id) && old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み + + + const target_prev_plugin = prev_map.get(downloaded_plugin_info.plugin_id); + const is_latest_version_available = (target_prev_plugin.is_downloaded) && !(downloaded_plugin_info.plugin_version === target_prev_plugin.latest_plugin_info.plugin_version); + + new_value = { + ...target_prev_plugin, + plugin_id: downloaded_plugin_info.plugin_id, component: component, is_downloaded: true, - is_latest_version_available: false, - } : old_value - ); - - return new_value; + is_latest_version_available: is_latest_version_available, + downloaded_plugin_info: { + ...downloaded_plugin_info, + component: component, + is_plugin_supported: is_plugin_supported, + is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, + }, + }; + } else { + new_value = old_plugin_data; + } + new_data.push(new_value); + } + return new_data; }); }, createAtomWithHook: (...args) => createAtomWithHook(...args), @@ -70,11 +104,11 @@ export const usePlugins = () => { const asyncLoadPlugin = async (plugin_folder_relative_path) => { const init_path = "plugins/" + plugin_folder_relative_path + "/index.esm.js"; - const plugin_info_path = "plugins/" + plugin_folder_relative_path + "/plugin_info.json"; + 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 plugin_info_json = await readTextFile(plugin_info_path, { dir: BaseDirectory.Resource, recursive: true }); - const plugin_info = JSON.parse(plugin_info_json); + 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); @@ -91,7 +125,7 @@ export const usePlugins = () => { URL.revokeObjectURL(blob_url); if (plugin_module && plugin_module.init) { - plugin_module.init(generatePluginContext(plugin_info)); + plugin_module.init(generatePluginContext(downloaded_plugin_info)); } await loadPluginCSS(plugin_css_path); @@ -102,16 +136,16 @@ export const usePlugins = () => { const asyncLoadAllPlugins = async () => { if (!import.meta.env.DEV) { - imported_dev_plugins.forEach(({ index, plugin_info }) => { - if (!index || !plugin_info) { - console.error("Invalid development plugin detected", index, plugin_info); + 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(plugin_info); + const plugin_context = generatePluginContext(downloaded_plugin_info); if (index.init) { index.init(plugin_context); } else { - console.error("Plugin missing init function", plugin_info); + console.error("Plugin missing init function", downloaded_plugin_info); } }); } else { @@ -128,8 +162,9 @@ export const usePlugins = () => { }; const downloadAndExtractPlugin = async (plugin) => { + const latest_plugin_info = plugin.latest_plugin_info; try { - const plugin_zip_url = await fetchLatestPluginZipUrl(plugin); + const plugin_zip_url = await fetchLatestPluginZipUrl(latest_plugin_info); // Rust コマンド経由で ZIP をダウンロード const base64_zip = await invoke("download_zip_asset", { url: plugin_zip_url }); // base64_zip をデコードして Uint8Array に変換 @@ -144,7 +179,7 @@ export const usePlugins = () => { const zip = await JSZip.loadAsync(bytes); // 展開先ディレクトリのパス(例:"plugins/" とする) - const target_plugin_path = "plugins/" + plugin.plugin_id; + 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 }); @@ -220,13 +255,13 @@ export const usePlugins = () => { plugins_data.map(async (plugin_data) => { try { const plugin_info = await asyncFetchPluginInfo(plugin_data.url); - return { ...plugin_info }; + return plugin_info; } catch (error) { console.error("Error fetching plugin info for URL:", plugin_data.url, error); - // エラー発生時は、plugin_data.title とエラーメッセージを返す return { title: plugin_data.title, plugin_id: plugin_data.plugin_id || plugin_data.title, + is_error: true, error: error.message, url: plugin_data.url }; @@ -254,14 +289,8 @@ export const usePlugins = () => { } const plugin_info = plugin_info_json_response.data; - const isPluginCompatible = (main_version, lower_version, upper_version) => { - console.log(main_version, lower_version, upper_version); - // lower_version 以上かつ upper_version 以下なら互換性ありと判定 - return semver.gte(main_version, lower_version) && semver.lte(main_version, upper_version); - }; - - const is_plugin_supported = isPluginCompatible(currentSoftwareVersion.data, plugin_info.min_supported_vrct_version, plugin_info.max_supported_vrct_version); + const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(plugin_info.min_supported_vrct_version, plugin_info.max_supported_vrct_version); return { title: plugin_info.title, @@ -270,21 +299,47 @@ export const usePlugins = () => { min_supported_vrct_version: plugin_info.min_supported_vrct_version, max_supported_vrct_version: plugin_info.max_supported_vrct_version, is_plugin_supported: is_plugin_supported, + is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, asset_name: plugin_info.asset_name, 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 setSavedPluginsStatus = (plugins_status) => { pendingSavedPluginsStatus(); asyncStdoutToPython("/set/data/plugins_status", plugins_status); }; + const isAnyPluginEnabled = () => { + return currentPluginsData.data.some(plugin => plugin.is_enabled); + }; + + const enabledPluginsList = () => { + return currentPluginsData.data.filter(plugin => plugin.is_enabled); + } + return { asyncFetchPluginsInfo, + isAnyPluginEnabled, + enabledPluginsList, + asyncLoadAllPlugins, downloadAndExtractPlugin, @@ -294,7 +349,12 @@ export const usePlugins = () => { currentPluginsData, updatePluginsData, + currentIsPluginsInitialized, + updateIsPluginsInitialized, + setSavedPluginsStatus, + + handlePendingPlugin, }; }; 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 4575c3d8..b73fe62e 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, @@ -105,7 +104,7 @@ export const useReceiveRoutes = () => { addSentMessageLog, addReceivedMessageLog, } = useMessage(); - const { updateIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable(); + const { updateLatestSoftwareVersionInfo } = useSoftwareVersion(); const { updateSoftwareVersion } = useSoftwareVersion(); const { updateEnableAutoMicSelect } = useEnableAutoMicSelect(); const { updateEnableAutoSpeakerSelect } = useEnableAutoSpeakerSelect(); @@ -207,7 +206,7 @@ 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": updateLatestSoftwareVersionInfo, "/run/connected_network": handleNetworkConnection, // Main Page diff --git a/src-ui/store.js b/src-ui/store.js index 7e88792b..5adbb4c7 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -114,7 +114,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: "", +}, "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"); @@ -275,7 +278,7 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA }, "Hotkeys"); // Plugins -// export const { atomInstance: Atom_LoadedPluginsList, useHook: useStore_LoadedPluginsList } = createAtomWithHook([], "LoadedPluginsList"); +export const { atomInstance: Atom_IsPluginsInitialized, useHook: useStore_IsPluginsInitialized } = createAtomWithHook(false, "IsPluginsInitialized"); export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([], "SavedPluginsStatus"); export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } = createAtomWithHook([], "PluginsData"); From 2157d5952c12124d654d8450d7cb19b653a70481 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:47:18 +0900 Subject: [PATCH 23/47] [UPdate] Add error boundary. --- .gitignore | 3 +- map-stack.js | 78 +++++++++++++ package-lock.json | 80 ++++++++----- package.json | 4 +- src-ui/app/App.jsx | 31 ++--- .../app/error_boundary/AppErrorBoundary.jsx | 89 ++++++++++++++ .../AppErrorBoundary.module.scss | 110 ++++++++++++++++++ .../contacts_container/ContactsContainer.jsx | 29 +++++ .../ContactsContainer.module.scss | 28 +++++ src-ui/assets/document.png | Bin 0 -> 1126 bytes vite.config.js | 1 + 11 files changed, 408 insertions(+), 45 deletions(-) create mode 100644 map-stack.js create mode 100644 src-ui/app/error_boundary/AppErrorBoundary.jsx create mode 100644 src-ui/app/error_boundary/AppErrorBoundary.module.scss create mode 100644 src-ui/app/error_boundary/contacts_container/ContactsContainer.jsx create mode 100644 src-ui/app/error_boundary/contacts_container/ContactsContainer.module.scss create mode 100644 src-ui/assets/document.png 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/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 62bd3e7f..85dac096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "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", "semver": "7.7.1" @@ -32,7 +33,8 @@ "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.2.1", + "source-map": "^0.7.4", + "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } }, @@ -210,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" @@ -264,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" }, @@ -283,13 +285,13 @@ } }, "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" @@ -313,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" @@ -342,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", @@ -4677,6 +4687,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", @@ -5108,11 +5129,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": { @@ -5495,9 +5517,9 @@ } }, "node_modules/vite": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz", - "integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==", + "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.25.0", "postcss": "^8.5.3", diff --git a/package.json b/package.json index 9de73950..de100d5d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "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", "semver": "7.7.1" @@ -47,7 +48,8 @@ "@tauri-apps/cli": "1.6.3", "npm-run-all": "4.1.5", "sass": "1.79.4", - "vite": "6.2.1", + "source-map": "0.7.4", + "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } } diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index c102af71..8c33ed37 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -22,6 +22,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(); @@ -32,22 +33,24 @@ export const App = () => { return (
- - - - - - - - - + + + + + + + + + + - {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) - ? - : - } + {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) + ? + : + } - + +
); }; 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/assets/document.png b/src-ui/assets/document.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe54e2d0491e01096a2f12c92b9242031399d1b GIT binary patch literal 1126 zcmeAS@N?(olHy`uVBq!ia0vp^A3&Ic4M^IBzMKT4I14-?iy0XBj({-ZRBb+K1_l-h zPZ!6Kid%2*TIVUdi#S}IcyLBz(AqP=)91@YD44C%q41$KG)Y;sjKat z&7dFm>B83!FVAhu-F@fxtTV5_e*Jp-@#Dw$-R<44%DdY$WLwYmtGBhcuebYUxr*h? zLzfzBMq?Ji`)&t#oR3@=HgFdFSjuvyz-7<;2FXbs-vtd03w=D!a^`}|pMCAs@dfYI zuKldp_j|A8@xA7sa&q>bWl=k*Qa8Kdq`=4XEIwTw`_&WpN_WQm+xg8}>7UmDnO9Gr zKJ^#8?+0YPeEBl;$aZ0acHxh2S9FOD+8%Qeso7vzg`0+c7PhZD=Ap=jPfBg+7g*G;HM%yMa zPTpR0gei?{wVO`DvW-bVrq*g#AaipNkm8%~CJoXwu^%c&l zNsNc}MU=!0_9TA+ipon^0tNUM1C70rd89-BUG2f#+j`Y@<#zMm*X1AE_`QC^tm^;o zeG@o@4O%!Q4zchgGPM~nI?rfO^f@4)mcU^x`mdKI;Vst0Hz(8&j0`b literal 0 HcmV?d00001 diff --git a/vite.config.js b/vite.config.js index 948083e4..b46e13f1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -35,6 +35,7 @@ export default defineConfig(async () => { main: path.resolve(__dirname, "index.html"), }, }, + sourcemap: true, }, resolve: { From d4219b5ce08de2ba31d3f180278e2ec2bc6dc9fe Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 11 Apr 2025 19:14:55 +0900 Subject: [PATCH 24/47] [Update/Refactor] Plugins: Organize codes. --- .../PluginsControlComponent.jsx | 47 +++---------------- .../setting_box/plugins/Plugins.jsx | 11 ++--- src-ui/logics/configs/plugins/usePlugins.js | 6 +-- 3 files changed, 14 insertions(+), 50 deletions(-) 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 index 0fd7c33a..0bdbfd02 100644 --- 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 @@ -3,14 +3,12 @@ import { SwitchBox } from "../index"; import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton"; import styles from "./PluginsControlComponent.module.scss"; -// メインのコントロールコンポーネント。ダウンロード済み / 未ダウンロードで分岐して表示する export const PluginsControlComponent = ({ variable_state, plugin_status, toggleFunction, downloadStartFunction, }) => { - // 共通オプション(各子コンポーネントに引き回す情報) const option = { id: plugin_status.plugin_id, is_pending: plugin_status.is_pending, @@ -41,84 +39,53 @@ export const PluginsControlComponent = ({ } }; -// ------------------------- -// ダウンロード済みのプラグイン用コンポーネント -// 状態により以下の分岐を行う -// ・ is_latest_version_already が true なら「最新版を使用中」 -// ・ is_latest_version_already が false かつ is_latest_version_available が true なら「最新版を利用可能」(アップデートボタン+スイッチ) -// ・ それ以外(is_latest_version_already:false && is_latest_version_available: false)なら、desc等の情報とスイッチのみ表示 + const DownloadedPluginControl = ({ option, plugin_status, toggleFunction, downloadStartFunction, }) => { - // on/off トグル時の処理 const togglePlugin = () => { toggleFunction(plugin_status.plugin_id); }; + const latest_version = plugin_status.latest_plugin_info?.plugin_version; - // ダウンロード済みの場合、ダウンロードされた情報からタイトルやバージョンを取得 - const title = plugin_status.downloaded_plugin_info?.title || plugin_status.latest_plugin_info.title; - const current_version = - plugin_status.downloaded_plugin_info?.plugin_version || plugin_status.latest_plugin_info.plugin_version; - - // コンポーネントごとに表示内容を分岐 if (plugin_status.is_latest_version_already) { - // 最新版が既に使用中 return (
-

{title}

-

現在のバージョン: {current_version}

+

最新のバージョン: {latest_version}

最新版を使用中

); } else if (plugin_status.is_latest_version_available) { - // 最新版の利用可能なお知らせとアップデートボタン+スイッチ return (
-

{title}

-

現在のバージョン: {current_version}

+

最新のバージョン: {latest_version}

最新版を利用可能

<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
); } else { - // 最新版利用可能ではないがダウンロード済み - // ※「desc」に関しては、情報があればplugin_status.descやlatest_plugin_info.descを利用してください - const desc = - plugin_status.latest_plugin_info.desc || - "追加情報がありません。"; return (
-

{title}

-

現在のバージョン: {current_version}

-

{desc}

+

最新のバージョン: {latest_version}

); } }; -// ------------------------- -// 未ダウンロードのプラグイン用コンポーネント -// 状態により以下の分岐を行う -// ・ is_latest_version_available が true なら:info_title, 最新バージョン情報とダウンロードボタン -// ・ is_latest_version_available が false なら:info_title, 最新バージョン情報と「現在利用不可」 + const NotDownloadedPluginControl = ({ option, plugin_status, downloadStartFunction }) => { - const title = plugin_status.latest_plugin_info.title; const latest_version = plugin_status.latest_plugin_info.plugin_version; - // ※未ダウンロードの場合、current_versionは「未ダウンロード」や「なし」といった扱いとする - const current_version = "未ダウンロード"; if (plugin_status.is_latest_version_available) { return (
-

{title}

-

現在のバージョン: {current_version}

最新バージョン: {latest_version}

<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
@@ -126,8 +93,6 @@ const NotDownloadedPluginControl = ({ option, plugin_status, downloadStartFuncti } else { return (
-

{title}

-

現在のバージョン: {current_version}

最新バージョン: {latest_version}

現在利用不可

diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 2a3fd42d..9baed8fb 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -79,20 +79,19 @@ const PluginDownloadContainer = () => {

{plugin.is_downloaded - ? plugin.downloaded_plugin_info?.title || plugin.latest_plugin_info.title - : plugin.latest_plugin_info.title} + ? plugin.downloaded_plugin_info?.title + : plugin.latest_plugin_info?.title}

{plugin.plugin_id}

- {plugin.error ? ( -

Error: {plugin.error}

+ {plugin.is_error ? ( +

Error: {plugin.error_message}

) : (
- {/* 状態に応じた情報表示(例:バージョン等) */}

{plugin.is_downloaded ? `現在のバージョン: ${plugin.downloaded_plugin_info?.plugin_version}` - : `最新バージョン: ${plugin.latest_plugin_info.plugin_version}`} + : null}

{ const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); const new_data = []; let new_value = {}; + const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(downloaded_plugin_info.min_supported_vrct_version, downloaded_plugin_info.max_supported_vrct_version); if (!prev_map.has(downloaded_plugin_info.plugin_id)) { // 未ダウンロード 新規登録 new_value = { @@ -67,9 +68,8 @@ export const usePlugins = () => { } for (const old_plugin_data of prev.data) { - const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(downloaded_plugin_info.min_supported_vrct_version, downloaded_plugin_info.max_supported_vrct_version); - if (prev_map.has(downloaded_plugin_info.plugin_id) && old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み + if (prev_map.has(downloaded_plugin_info.plugin_id) && old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み アップデート const target_prev_plugin = prev_map.get(downloaded_plugin_info.plugin_id); @@ -262,7 +262,7 @@ export const usePlugins = () => { title: plugin_data.title, plugin_id: plugin_data.plugin_id || plugin_data.title, is_error: true, - error: error.message, + error_message: error.message, url: plugin_data.url }; } From 1bea61c45a48aec161e1d4e26ebe6320e02365af Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 12 Apr 2025 14:28:01 +0900 Subject: [PATCH 25/47] [Update] Add dev plugin mode and dev plugin list. --- src-tauri/tauri.conf.json | 7 ++++++- .../PluginsControlComponent.jsx | 2 +- src-ui/logics/configs/plugins/usePlugins.js | 13 +++++++++---- src-ui/ui_configs.js | 9 +++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b36e8ce4..a23b5b08 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -41,7 +41,12 @@ }, "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"] + "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, 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 index 0bdbfd02..4408d6cc 100644 --- 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 @@ -81,7 +81,7 @@ const DownloadedPluginControl = ({ const NotDownloadedPluginControl = ({ option, plugin_status, downloadStartFunction }) => { - const latest_version = plugin_status.latest_plugin_info.plugin_version; + const latest_version = plugin_status.latest_plugin_info?.plugin_version; if (plugin_status.is_latest_version_available) { return ( diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 682a6d40..ef1acd04 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/tauri"; +import { IS_PLUGIN_DEV_MODE, getPluginsList } from "@ui_configs"; import { createAtomWithHook, useStore_SavedPluginsStatus, @@ -14,7 +15,7 @@ const imported_dev_plugins = []; dev_plugins.forEach(async ({entry_path}) => { imported_dev_plugins.push({ index: await import(`@plugins_path/${entry_path}/index.jsx`), - plugin_info: await import(`@plugins_path/${entry_path}/plugin_info.json`), + downloaded_plugin_info: await import(`@plugins_path/${entry_path}/plugin_info.json`), }); }) @@ -28,7 +29,7 @@ import * as logics_common from "@logics_common"; // PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL -const PLUGIN_LIST_URL = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json"; +const PLUGIN_LIST_URL = getPluginsList(); export const usePlugins = () => { const { asyncStdoutToPython } = useStdoutToPython(); @@ -135,7 +136,7 @@ export const usePlugins = () => { }; const asyncLoadAllPlugins = async () => { - if (!import.meta.env.DEV) { + if (IS_PLUGIN_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); @@ -150,7 +151,9 @@ export const usePlugins = () => { }); } else { try { - const plugin_files = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true }); + 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); @@ -368,9 +371,11 @@ const removeImportStatements = (code) => { // 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, "")}`; diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index 4bee9957..f5f89b0c 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -52,6 +52,15 @@ export const ui_configs = { ] }; +export const IS_PLUGIN_DEV_MODE = false; +export const getPluginsList = () => { + const base_url = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/"; + const plugins_list_url = (IS_PLUGIN_DEV_MODE) + ? base_url + "dev_vrct_plugins_list.json" + : base_url + "vrct_plugins_list.json"; + return plugins_list_url; +}; + export const translator_status = [ { id: "DeepL", label: "DeepL", is_available: false }, { id: "DeepL_API", label: `DeepL API`, is_available: false }, From ddc6408828e33c35b442d9a8647411f78ddf4ef4 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 13 Apr 2025 17:14:54 +0900 Subject: [PATCH 26/47] [Update/bugfix] Fix handling flag 'is_latest_version_already' that was overrode unexpectedly. Add some patterns to plugin list. it shows update-able if the user update vrct. Add auto update function if the plugin is enabled. Add safety that turn to disable the plugin if it's not supported with current vrct version. --- .../_app_controllers/PluginsController.jsx | 32 ++++++++++++++++--- .../PluginsControlComponent.jsx | 18 +++++++++-- src-ui/logics/configs/plugins/usePlugins.js | 10 +++--- src-ui/ui_configs.js | 1 + 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 2fad4376..98327baf 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -15,6 +15,7 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { updatePluginsData, currentSavedPluginsStatus, updateIsPluginsInitialized, + downloadAndExtractPlugin, } = usePlugins(); useEffect(() => { @@ -36,19 +37,19 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { const is_latest_version_available = !(target_downloaded_plugin.plugin_version === info.plugin_version); new_plugin_info = { - is_downloaded: true, - is_latest_version_already: (target_downloaded_plugin.downloaded_plugin_info?.plugin_version === info.plugin_version), - is_latest_version_available: is_latest_version_available, - latest_plugin_info: { ...info }, ...target_downloaded_plugin, + is_downloaded: true, + latest_plugin_info: { ...info }, + is_latest_version_available: is_latest_version_available, + is_latest_version_already: (target_downloaded_plugin.downloaded_plugin_info?.plugin_version === info.plugin_version), }; } else { // infoにもあり登録済みだがダウンロードされていない new_plugin_info = { + ...target_downloaded_plugin, is_downloaded: false, is_latest_version_already: false, is_latest_version_available: info.is_latest_version_available, latest_plugin_info: { ...info }, - ...target_downloaded_plugin, } } } else { // 未ダウンロード @@ -75,8 +76,29 @@ export const PluginsController = ({ fetchPluginsHasRunRef }) => { plugin.is_latest_version_available = (plugin.latest_plugin_info.is_plugin_supported); } }); + + // ダウンロード済みで最新版じゃない場合、自動的にアップデート + // is_latest_version_supported: true のみ。 + // 失敗した場合、現在のバージョンが非対応の場合はdisabledにする。 + new_data.forEach(async plugin => { + if (plugin.is_enabled) { + if (!plugin.is_latest_version_already && plugin.is_latest_version_available) { + await downloadAndExtractPlugin(plugin); + } + } + }); + + new_data.forEach(async plugin => { + if (plugin.is_enabled) { + if (!plugin.downloaded_plugin_info?.is_plugin_supported) { + plugin.is_enabled = false + } + } + }); + return new_data; }); + } catch (error) { console.error(error); } 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 index 4408d6cc..e6ddd500 100644 --- 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 @@ -52,7 +52,14 @@ const DownloadedPluginControl = ({ const latest_version = plugin_status.latest_plugin_info?.plugin_version; - if (plugin_status.is_latest_version_already) { + + if (!plugin_status.downloaded_plugin_info.is_plugin_supported) { + return ( +
+

現在利用不可 使用中VRCTバージョンとの互換性なし

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

最新のバージョン: {latest_version}

@@ -72,7 +79,7 @@ const DownloadedPluginControl = ({ } else { return (
-

最新のバージョン: {latest_version}

+

最新版は現在利用不可

); @@ -90,6 +97,13 @@ const NotDownloadedPluginControl = ({ option, plugin_status, downloadStartFuncti <_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
); + } else if (plugin_status.latest_plugin_info?.is_plugin_supported_latest_vrct) { + return ( +
+

最新のバージョン: {latest_version}

+

VRCT最新版で利用可能

+
+ ); } else { return (
diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index ef1acd04..9f1be315 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -52,12 +52,14 @@ export const usePlugins = () => { let new_value = {}; const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(downloaded_plugin_info.min_supported_vrct_version, downloaded_plugin_info.max_supported_vrct_version); - if (!prev_map.has(downloaded_plugin_info.plugin_id)) { // 未ダウンロード 新規登録 + const target_plugin = prev_map.get(downloaded_plugin_info.plugin_id); + if (!target_plugin) { // 未ダウンロード 新規登録 new_value = { plugin_id: downloaded_plugin_info.plugin_id, component: component, is_downloaded: true, is_latest_version_available: false, + is_latest_version_already: true, downloaded_plugin_info: { ...downloaded_plugin_info, component: component, @@ -70,9 +72,7 @@ export const usePlugins = () => { for (const old_plugin_data of prev.data) { - if (prev_map.has(downloaded_plugin_info.plugin_id) && old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み アップデート - - + if (old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み or 登録済 アップデート const target_prev_plugin = prev_map.get(downloaded_plugin_info.plugin_id); const is_latest_version_available = (target_prev_plugin.is_downloaded) && !(downloaded_plugin_info.plugin_version === target_prev_plugin.latest_plugin_info.plugin_version); @@ -82,6 +82,7 @@ export const usePlugins = () => { component: component, is_downloaded: true, is_latest_version_available: is_latest_version_available, + is_latest_version_already: (target_plugin.plugin_version === old_plugin_data.latest_plugin_info?.plugin_info), downloaded_plugin_info: { ...downloaded_plugin_info, component: component, @@ -168,6 +169,7 @@ export const usePlugins = () => { 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 に変換 diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index f5f89b0c..6db3193f 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -53,6 +53,7 @@ export const ui_configs = { }; export const IS_PLUGIN_DEV_MODE = false; +if (IS_PLUGIN_DEV_MODE) console.warn("ui_configs IS_PLUGIN_DEV_MODE: true. Turn to 'false' when it's production environment."); export const getPluginsList = () => { const base_url = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/"; const plugins_list_url = (IS_PLUGIN_DEV_MODE) From 379ca86b4564bda744a0e517194d04216644228a Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:48:50 +0900 Subject: [PATCH 27/47] [Refactor/bugfix/TMP] TMP: Separate plugins controller. Add ui pattern for outdated plugins and the plugin that is not supported current vrct version but supported in newest vrct version. --- src-ui/app/App.jsx | 5 +- .../_app_controllers/PluginsController.jsx | 151 ++---------------- .../FetchLatestPluginsDataController.jsx | 115 +++++++++++++ .../LoadPluginsController.jsx | 36 +++++ .../MergeSavedPluginsStatusController.jsx | 35 ++++ .../PluginsControlComponent.jsx | 16 ++ .../update_modal/UpdateModal.jsx | 14 +- src-ui/logics/configs/plugins/usePlugins.js | 18 ++- src-ui/logics/useReceiveRoutes.js | 8 +- src-ui/store.js | 3 + 10 files changed, 251 insertions(+), 150 deletions(-) create mode 100644 src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx create mode 100644 src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx create mode 100644 src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index 8c33ed37..f8314681 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -29,7 +29,10 @@ export const App = () => { const { currentIsBackendReady } = useIsBackendReady(); const { WindowGeometryController } = useWindow(); const { i18n } = useTranslation(); - const fetchPluginsHasRunRef = useRef(false); + const fetchPluginsHasRunRef = useRef({ + is_initialized_load_plugin: false, + is_fetched_plugins_info: false, + }); return (
diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 98327baf..9939bfe3 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -1,142 +1,15 @@ -import React, { useEffect, useRef } from "react"; -import { usePlugins } from "@logics_configs"; -import clsx from "clsx"; +import React, { useEffect } from "react"; +import { LoadPluginsController } from "./plugins_controllers/LoadPluginsController"; +import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController"; +import { MergeSavedPluginsStatusController } from "./plugins_controllers/MergeSavedPluginsStatusController"; -if (typeof window !== "undefined") { - window.React = React; - window.clsx = clsx; -} +export const PluginsController = () => { -export const PluginsController = ({ fetchPluginsHasRunRef }) => { - const { - asyncLoadAllPlugins, - asyncFetchPluginsInfo, - currentPluginsData, - updatePluginsData, - currentSavedPluginsStatus, - updateIsPluginsInitialized, - downloadAndExtractPlugin, - } = usePlugins(); - - useEffect(() => { - const loadPlugins = async () => { - try { - await asyncLoadAllPlugins(); - const info_array = await asyncFetchPluginsInfo(); - updatePluginsData(prev => { - // Map を利用してそれぞれの配列を plugin_id で参照できるようにする - const info_map = new Map(info_array.map(info => [info.plugin_id, info])); - const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); - - const new_data = []; - for (const info of info_array) { - let new_plugin_info = {}; - if (prev_map.has(info.plugin_id)) { // plugin_id 登録済み - const target_downloaded_plugin = prev_map.get(info.plugin_id); - if (target_downloaded_plugin.is_downloaded) { // 既にダウンロード済み - const is_latest_version_available = !(target_downloaded_plugin.plugin_version === info.plugin_version); - - new_plugin_info = { - ...target_downloaded_plugin, - is_downloaded: true, - latest_plugin_info: { ...info }, - is_latest_version_available: is_latest_version_available, - is_latest_version_already: (target_downloaded_plugin.downloaded_plugin_info?.plugin_version === info.plugin_version), - }; - } else { // infoにもあり登録済みだがダウンロードされていない - new_plugin_info = { - ...target_downloaded_plugin, - is_downloaded: false, - is_latest_version_already: false, - is_latest_version_available: info.is_latest_version_available, - latest_plugin_info: { ...info }, - } - } - } else { // 未ダウンロード - new_plugin_info = { - plugin_id: info.plugin_id, - is_downloaded: false, - is_latest_version_already: false, - latest_plugin_info: { ...info }, - }; - } - new_data.push(new_plugin_info); - } - - // prev.data にのみ存在するアイテム = latest plugin infoには存在しない - // を追加し、is_outdated: true を付与 - prev.data.forEach(item => { - if (!info_map.has(item.plugin_id)) { - new_data.push({ ...item, is_outdated: true }); - } - }); - - new_data.forEach(plugin => { - if (!plugin.is_outdated) { - plugin.is_latest_version_available = (plugin.latest_plugin_info.is_plugin_supported); - } - }); - - // ダウンロード済みで最新版じゃない場合、自動的にアップデート - // is_latest_version_supported: true のみ。 - // 失敗した場合、現在のバージョンが非対応の場合はdisabledにする。 - new_data.forEach(async plugin => { - if (plugin.is_enabled) { - if (!plugin.is_latest_version_already && plugin.is_latest_version_available) { - await downloadAndExtractPlugin(plugin); - } - } - }); - - new_data.forEach(async plugin => { - if (plugin.is_enabled) { - if (!plugin.downloaded_plugin_info?.is_plugin_supported) { - plugin.is_enabled = false - } - } - }); - - return new_data; - }); - - } catch (error) { - console.error(error); - } - }; - - if (!fetchPluginsHasRunRef.current) { - loadPlugins(); - updateIsPluginsInitialized(true); - } - return () => fetchPluginsHasRunRef.current = true; - }, []); - - - useEffect(() => { - updatePluginsData(prev => { - // currentSavedPluginsStatus.data の各要素を Map 化して plugin_id で参照 - 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])); - // prev.data にある各アイテムについて、保存済みの状態情報があればマージ - const merged = prev.data.map(item => { - - if (saved_map.has(item.plugin_id)) { - return { ...item, is_enabled: saved_map.get(item.plugin_id).is_enabled }; - } - return item; - }); - - // currentSavedPluginsStatus.data にのみ存在する項目があれば追加 - currentSavedPluginsStatus.data.forEach(saved => { - if (!prev_map.has(saved.plugin_id)) { - merged.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); - } - }); - return merged; - }); - }, [currentSavedPluginsStatus]); - - - - return null; + return ( + <> + + + + + ); }; \ 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..e0fde914 --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx @@ -0,0 +1,115 @@ +import { useEffect } from "react"; +import { usePlugins } from "@logics_configs"; + +export const FetchLatestPluginsDataController = () => { + const { + asyncFetchPluginsInfo, + updatePluginsData, + downloadAndExtractPlugin, + currentIsInitializedLoadPlugin, + currentIsFetchedPluginsInfo, + updateIsFetchedPluginsInfo, + } = usePlugins(); + + const asyncUpdateLatestPluginsData = async () => { + try { + const info_array = await asyncFetchPluginsInfo(); + updatePluginsData(prev => { + // Map を利用してそれぞれの配列を plugin_id で参照できるようにする + const info_map = new Map(info_array.map(info => [info.plugin_id, info])); + const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); + + console.log(prev_map); + + const new_data = []; + for (const info of info_array) { + let new_plugin_info = {}; + if (prev_map.has(info.plugin_id)) { // plugin_id 登録済み + const target_downloaded_plugin = prev_map.get(info.plugin_id); + + if (target_downloaded_plugin.is_downloaded) { // 既にダウンロード済み + const is_latest_version_available = !(target_downloaded_plugin.plugin_version === info.plugin_version); + + new_plugin_info = { + ...target_downloaded_plugin, + is_downloaded: true, + latest_plugin_info: { ...info }, + is_latest_version_available: is_latest_version_available, + is_latest_version_already: (target_downloaded_plugin.downloaded_plugin_info?.plugin_version === info.plugin_version), + }; + } else { // infoにもあり登録済みだがダウンロードされていない + new_plugin_info = { + ...target_downloaded_plugin, + is_downloaded: false, + is_latest_version_already: false, + is_latest_version_available: info.is_latest_version_available, + latest_plugin_info: { ...info }, + } + } + } else { // 未ダウンロード + new_plugin_info = { + plugin_id: info.plugin_id, + is_downloaded: false, + is_latest_version_already: false, + latest_plugin_info: { ...info }, + }; + } + + new_data.push(new_plugin_info); + } + + // prev.data にのみ存在するアイテム = latest plugin infoには存在しない + // を追加し、is_outdated: true を付与 + prev.data.forEach(item => { + if (!info_map.has(item.plugin_id)) { + new_data.push({ ...item, is_outdated: true }); + } + }); + + new_data.forEach(plugin => { + if (!plugin.is_outdated) { + plugin.is_latest_version_available = (plugin.latest_plugin_info.is_plugin_supported); + } + }); + + // ダウンロード済みで最新版じゃない場合、自動的にアップデート + // is_latest_version_supported: true のみ。 + // 失敗した場合、現在のバージョンが非対応の場合はdisabledにする。 + new_data.forEach(async plugin => { + if (plugin.is_enabled) { + console.log(plugin); + + if (!plugin.is_latest_version_already && plugin.is_latest_version_available) { + await downloadAndExtractPlugin(plugin); + } + } + }); + + new_data.forEach(async plugin => { + if (plugin.is_downloaded && plugin.is_enabled) { + if (!plugin.downloaded_plugin_info?.is_plugin_supported && !plugin.latest_plugin_info?.is_plugin_supported) { + plugin.is_enabled = false + } + } + }); + + return new_data; + }); + + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + console.log(currentIsInitializedLoadPlugin.data); + if (currentIsInitializedLoadPlugin.data && !currentIsFetchedPluginsInfo.data) { + asyncUpdateLatestPluginsData().then(() => { + updateIsFetchedPluginsInfo(true); + }); + } + + }, [currentIsInitializedLoadPlugin.data]); + + 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..23422cc5 --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx @@ -0,0 +1,36 @@ +import React, { useEffect } from "react"; +import { usePlugins } from "@logics_configs"; +import clsx from "clsx"; + +if (typeof window !== "undefined") { + window.React = React; + window.clsx = clsx; +} + +export const LoadPluginsController = () => { + + const { + asyncLoadAllPlugins, + currentIsInitializedLoadPlugin, + updateIsInitializedLoadPlugin, + } = usePlugins(); + + const asyncInitLoadPlugins = async () => { + try { + await asyncLoadAllPlugins(); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + + if (!currentIsInitializedLoadPlugin.data) { + asyncInitLoadPlugins().then(() => { + updateIsInitializedLoadPlugin(true); + }); + } + }, []); + + return null; +}; \ No newline at end of file diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx new file mode 100644 index 00000000..9b3dcb23 --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx @@ -0,0 +1,35 @@ +import { useEffect } from "react"; +import { usePlugins } from "@logics_configs"; + +export const MergeSavedPluginsStatusController = () => { + const { + updatePluginsData, + currentSavedPluginsStatus, + } = usePlugins(); + + useEffect(() => { + updatePluginsData(prev => { + // currentSavedPluginsStatus.data の各要素を Map 化して plugin_id で参照 + 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])); + // prev.data にある各アイテムについて、保存済みの状態情報があればマージ + const merged = prev.data.map(item => { + + if (saved_map.has(item.plugin_id)) { + return { ...item, is_enabled: saved_map.get(item.plugin_id).is_enabled }; + } + return item; + }); + + // currentSavedPluginsStatus.data にのみ存在する項目があれば追加 + currentSavedPluginsStatus.data.forEach(saved => { + if (!prev_map.has(saved.plugin_id)) { + merged.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); + } + }); + return merged; + }); + }, [currentSavedPluginsStatus]); + + return null; +}; \ 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 index e6ddd500..9c33c90d 100644 --- 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 @@ -54,11 +54,27 @@ const DownloadedPluginControl = ({ if (!plugin_status.downloaded_plugin_info.is_plugin_supported) { + if (plugin_status.is_latest_version_available) { + return ( +
+

最新のバージョン: {latest_version}

+

最新版にアップデート後 利用可能

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

現在利用不可 使用中VRCTバージョンとの互換性なし

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

最新情報が取得できません

+ +
+ ); } else if (plugin_status.is_latest_version_already) { return (
diff --git a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx index 4fef1c71..caf89d43 100644 --- a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx +++ b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx @@ -20,9 +20,6 @@ export const UpdateModal = () => { const { currentLatestSoftwareVersionInfo } = useSoftwareVersion(); const { isAnyPluginEnabled } = usePlugins(); - console.log(isAnyPluginEnabled()); - - const is_latest_version_already = currentLatestSoftwareVersionInfo.data.is_update_available === false; const is_cpu_version = currentComputeMode.data === "cpu"; @@ -101,14 +98,17 @@ const CurrentVersionLabel = (props) => { const PluginUpdateNotification = () => { const { enabledPluginsList } = usePlugins(); - const incompatible_plugins_list = enabledPluginsList(); + // ダウンロード済みのもの or プラグイン最新版が、VRCT最新版(VRCTアプデ後)に非対応のもの + const incompatible_plugins_list = enabledPluginsList().filter(plugin => { + if (!plugin.is_downloaded) return false; + if (!plugin.downloaded_plugin_info?.is_plugin_supported_latest_vrct || !plugin.latest_plugin_info.is_plugin_supported_latest_vrct) return true; + }); return (
{incompatible_plugins_list.map(plugin => { - console.log(plugin); - - return

{plugin.title}

+ const target_data = plugin.downloaded_plugin_info; + return

{target_data.title}

})}
); diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 9f1be315..aa8c5ad7 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -5,6 +5,8 @@ import { useStore_SavedPluginsStatus, useStore_PluginsData, useStore_IsPluginsInitialized, + useStore_IsInitializedLoadPlugin, + useStore_IsFetchedPluginsInfo, } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; @@ -33,6 +35,11 @@ const PLUGIN_LIST_URL = getPluginsList(); export const usePlugins = () => { const { asyncStdoutToPython } = useStdoutToPython(); + + const { currentIsInitializedLoadPlugin, updateIsInitializedLoadPlugin, pendingIsInitializedLoadPlugin } = useStore_IsInitializedLoadPlugin(); + const { currentIsFetchedPluginsInfo, updateIsFetchedPluginsInfo, pendingIsFetchedPluginsInfo } = useStore_IsFetchedPluginsInfo(); + + const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); const { currentIsPluginsInitialized, updateIsPluginsInitialized, pendingIsPluginsInitialized } = useStore_IsPluginsInitialized(); @@ -40,13 +47,15 @@ export const usePlugins = () => { const { asyncTauriFetchGithub } = useFetch(); - const generatePluginContext= (downloaded_plugin_info) => { + 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); } updatePluginsData(prev => { + console.log("-----updated downloaded plugin info----"); + const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); const new_data = []; let new_value = {}; @@ -74,7 +83,7 @@ export const usePlugins = () => { if (old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み or 登録済 アップデート const target_prev_plugin = prev_map.get(downloaded_plugin_info.plugin_id); - const is_latest_version_available = (target_prev_plugin.is_downloaded) && !(downloaded_plugin_info.plugin_version === target_prev_plugin.latest_plugin_info.plugin_version); + const is_latest_version_available = (target_prev_plugin.is_downloaded) && (target_prev_plugin.latest_plugin_info?.plugin_version) && !(downloaded_plugin_info.plugin_version === target_prev_plugin.latest_plugin_info?.plugin_version); new_value = { ...target_prev_plugin, @@ -357,6 +366,11 @@ export const usePlugins = () => { currentIsPluginsInitialized, updateIsPluginsInitialized, + currentIsInitializedLoadPlugin, + updateIsInitializedLoadPlugin, + currentIsFetchedPluginsInfo, + updateIsFetchedPluginsInfo, + setSavedPluginsStatus, handlePendingPlugin, diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index b73fe62e..fb826f8c 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -206,7 +206,13 @@ 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/software_update_info": updateLatestSoftwareVersionInfo, + "/run/software_update_info": () => { + updateLatestSoftwareVersionInfo({ + is_update_available: true, + new_version: "3.0.3", + }) + }, + // "/run/software_update_info": updateLatestSoftwareVersionInfo, "/run/connected_network": handleNetworkConnection, // Main Page diff --git a/src-ui/store.js b/src-ui/store.js index 5adbb4c7..58012777 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -279,6 +279,9 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA // Plugins export const { atomInstance: Atom_IsPluginsInitialized, useHook: useStore_IsPluginsInitialized } = createAtomWithHook(false, "IsPluginsInitialized"); +export const { atomInstance: Atom_IsInitializedLoadPlugin, useHook: useStore_IsInitializedLoadPlugin } = createAtomWithHook(false, "IsInitializedLoadPlugin"); +export const { atomInstance: Atom_IsFetchedPluginsInfo, useHook: useStore_IsFetchedPluginsInfo } = createAtomWithHook(false, "IsFetchedPluginsInfo"); +export const { atomInstance: Atom_FetchedPluginsInfo, useHook: useStore_FetchedPluginsInfo } = createAtomWithHook(false, "FetchedPluginsInfo"); export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([], "SavedPluginsStatus"); export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } = createAtomWithHook([], "PluginsData"); From 4c12e8b94644b0ae0f5904cb2bb833e7cde7aacf Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 16 Apr 2025 19:02:41 +0900 Subject: [PATCH 28/47] [Update/Refactor] For data integration correctly, the plugins data (saved, downloaded, fetched) merge whenever update data each of it. Separate plugins controllers. --- src-ui/app/App.jsx | 10 +- .../_app_controllers/PluginsController.jsx | 21 +- .../FetchLatestPluginsDataController.jsx | 104 +-------- .../LoadPluginsController.jsx | 14 +- .../MergePluginsController.jsx | 207 ++++++++++++++++++ .../setting_box/plugins/Plugins.jsx | 2 +- src-ui/logics/configs/plugins/usePlugins.js | 81 +++---- src-ui/store.js | 4 +- 8 files changed, 268 insertions(+), 175 deletions(-) create mode 100644 src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index f8314681..9c7e8aa0 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -29,9 +29,9 @@ export const App = () => { const { currentIsBackendReady } = useIsBackendReady(); const { WindowGeometryController } = useWindow(); const { i18n } = useTranslation(); - const fetchPluginsHasRunRef = useRef({ + const pluginsControllerHasRunRef = useRef({ is_initialized_load_plugin: false, - is_fetched_plugins_info: false, + is_init_fetched_plugins_info: false, }); return ( @@ -49,7 +49,7 @@ export const App = () => { {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) ? - : + : } @@ -58,11 +58,11 @@ export const App = () => { ); }; -const Contents = ({ fetchPluginsHasRunRef }) => { +const Contents = ({ pluginsControllerHasRunRef }) => { 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 index 9939bfe3..9840afd5 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -1,15 +1,24 @@ -import React, { useEffect } from "react"; +import React from "react"; +import clsx from "clsx"; + +if (typeof window !== "undefined") { + window.React = React; + window.clsx = clsx; +} + import { LoadPluginsController } from "./plugins_controllers/LoadPluginsController"; import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController"; -import { MergeSavedPluginsStatusController } from "./plugins_controllers/MergeSavedPluginsStatusController"; +// import { MergeSavedPluginsStatusController } from "./plugins_controllers/MergeSavedPluginsStatusController"; +import { MergePluginsController } from "./plugins_controllers/MergePluginsController"; -export const PluginsController = () => { +export const PluginsController = ({ pluginsControllerHasRunRef }) => { return ( <> - - - + + + + {/* */} ); }; \ 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 index e0fde914..088d9cae 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx @@ -1,115 +1,25 @@ import { useEffect } from "react"; import { usePlugins } from "@logics_configs"; -export const FetchLatestPluginsDataController = () => { +export const FetchLatestPluginsDataController = ({ pluginsControllerHasRunRef }) => { const { asyncFetchPluginsInfo, - updatePluginsData, - downloadAndExtractPlugin, - currentIsInitializedLoadPlugin, - currentIsFetchedPluginsInfo, - updateIsFetchedPluginsInfo, } = usePlugins(); - const asyncUpdateLatestPluginsData = async () => { + const asyncInitFetchPluginsInfo = async () => { try { - const info_array = await asyncFetchPluginsInfo(); - updatePluginsData(prev => { - // Map を利用してそれぞれの配列を plugin_id で参照できるようにする - const info_map = new Map(info_array.map(info => [info.plugin_id, info])); - const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); - - console.log(prev_map); - - const new_data = []; - for (const info of info_array) { - let new_plugin_info = {}; - if (prev_map.has(info.plugin_id)) { // plugin_id 登録済み - const target_downloaded_plugin = prev_map.get(info.plugin_id); - - if (target_downloaded_plugin.is_downloaded) { // 既にダウンロード済み - const is_latest_version_available = !(target_downloaded_plugin.plugin_version === info.plugin_version); - - new_plugin_info = { - ...target_downloaded_plugin, - is_downloaded: true, - latest_plugin_info: { ...info }, - is_latest_version_available: is_latest_version_available, - is_latest_version_already: (target_downloaded_plugin.downloaded_plugin_info?.plugin_version === info.plugin_version), - }; - } else { // infoにもあり登録済みだがダウンロードされていない - new_plugin_info = { - ...target_downloaded_plugin, - is_downloaded: false, - is_latest_version_already: false, - is_latest_version_available: info.is_latest_version_available, - latest_plugin_info: { ...info }, - } - } - } else { // 未ダウンロード - new_plugin_info = { - plugin_id: info.plugin_id, - is_downloaded: false, - is_latest_version_already: false, - latest_plugin_info: { ...info }, - }; - } - - new_data.push(new_plugin_info); - } - - // prev.data にのみ存在するアイテム = latest plugin infoには存在しない - // を追加し、is_outdated: true を付与 - prev.data.forEach(item => { - if (!info_map.has(item.plugin_id)) { - new_data.push({ ...item, is_outdated: true }); - } - }); - - new_data.forEach(plugin => { - if (!plugin.is_outdated) { - plugin.is_latest_version_available = (plugin.latest_plugin_info.is_plugin_supported); - } - }); - - // ダウンロード済みで最新版じゃない場合、自動的にアップデート - // is_latest_version_supported: true のみ。 - // 失敗した場合、現在のバージョンが非対応の場合はdisabledにする。 - new_data.forEach(async plugin => { - if (plugin.is_enabled) { - console.log(plugin); - - if (!plugin.is_latest_version_already && plugin.is_latest_version_available) { - await downloadAndExtractPlugin(plugin); - } - } - }); - - new_data.forEach(async plugin => { - if (plugin.is_downloaded && plugin.is_enabled) { - if (!plugin.downloaded_plugin_info?.is_plugin_supported && !plugin.latest_plugin_info?.is_plugin_supported) { - plugin.is_enabled = false - } - } - }); - - return new_data; - }); - + await asyncFetchPluginsInfo(); } catch (error) { console.error(error); } }; useEffect(() => { - console.log(currentIsInitializedLoadPlugin.data); - if (currentIsInitializedLoadPlugin.data && !currentIsFetchedPluginsInfo.data) { - asyncUpdateLatestPluginsData().then(() => { - updateIsFetchedPluginsInfo(true); - }); + if (!pluginsControllerHasRunRef.current.is_init_fetched_plugins_info) { + asyncInitFetchPluginsInfo(); + pluginsControllerHasRunRef.current.is_init_fetched_plugins_info = true; } - - }, [currentIsInitializedLoadPlugin.data]); + }, []); 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 index 23422cc5..0945df96 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx @@ -1,17 +1,9 @@ import React, { useEffect } from "react"; import { usePlugins } from "@logics_configs"; -import clsx from "clsx"; - -if (typeof window !== "undefined") { - window.React = React; - window.clsx = clsx; -} - -export const LoadPluginsController = () => { +export const LoadPluginsController = ({ pluginsControllerHasRunRef }) => { const { asyncLoadAllPlugins, - currentIsInitializedLoadPlugin, updateIsInitializedLoadPlugin, } = usePlugins(); @@ -24,11 +16,11 @@ export const LoadPluginsController = () => { }; useEffect(() => { - - if (!currentIsInitializedLoadPlugin.data) { + if (!pluginsControllerHasRunRef.current.is_initialized_load_plugin) { asyncInitLoadPlugins().then(() => { updateIsInitializedLoadPlugin(true); }); + pluginsControllerHasRunRef.current.is_initialized_load_plugin = true; } }, []); 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..2fdb51c9 --- /dev/null +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -0,0 +1,207 @@ +import { useEffect, useRef } from "react"; +import { usePlugins } from "@logics_configs"; +import { useSoftwareVersion } from "@logics_common"; + +export const MergePluginsController = () => { + const { + currentLoadedPlugins, + updatePluginsData, + currentPluginsData, + currentFetchedPluginsInfo, + currentSavedPluginsStatus, + downloadAndExtractPlugin, + } = usePlugins(); + const { checkVrctVerCompatibility } = useSoftwareVersion(); + + // 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 }); + } + } + + + // 追加処理: ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化 + new_data.forEach(plugin => { + if (plugin.is_downloaded && plugin.is_enabled) { + if ( + !plugin.downloaded_plugin_info?.is_plugin_supported && + !plugin.latest_plugin_info?.is_plugin_supported + ) { + plugin.is_enabled = false; + } + } + }); + + console.log("merged plugin data", new_data); + return new_data; + }); + }; + + mergePluginData(); + }, [currentFetchedPluginsInfo.data, currentLoadedPlugins.data, currentSavedPluginsStatus]); + + + + // --- 自動アップデート(ダウンロード処理)のuseEffect --- + // ※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.is_latest_version_already && + plugin.is_latest_version_available + ) { + console.log(!downloadingRef.current.has(plugin.plugin_id)); + + if (!downloadingRef.current.has(plugin.plugin_id)) { + downloadingRef.current.add(plugin.plugin_id); + // ※ downloadAndExtractPlugin は外部通信を伴い currentLoadedPlugins.data 更新を引き起こすので注意 + downloadAndExtractPlugin(plugin) + .then(() => { + console.log(`Plugin ${plugin.plugin_id} updated successfully`); + downloadingRef.current.delete(plugin.plugin_id); + }) + .catch((error) => { + console.error(`Plugin ${plugin.plugin_id} update failed`, error); + downloadingRef.current.delete(plugin.plugin_id); + }); + } + } + }); + }, [currentPluginsData.data]); + + + + + return null; +}; + + + + + +// ダウンロード済みで最新版じゃない場合、自動的にアップデート +// // is_latest_version_supported: true のみ。 +// // 失敗した場合、現在のバージョンが非対応の場合はdisabledにする。 +// new_data.forEach(async plugin => { +// if (plugin.is_enabled) { +// console.log(plugin); + +// if (!plugin.is_latest_version_already && plugin.is_latest_version_available) { +// await downloadAndExtractPlugin(plugin); +// } +// } +// }); + +// new_data.forEach(plugin => { +// if (plugin.is_downloaded && plugin.is_enabled) { +// if (!plugin.downloaded_plugin_info?.is_plugin_supported && !plugin.latest_plugin_info?.is_plugin_supported) { +// plugin.is_enabled = false +// } +// } +// }); \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 9baed8fb..9f7f490e 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -8,7 +8,7 @@ export const Plugins = () => { currentIsPluginsInitialized, } = usePlugins(); - if (!currentIsPluginsInitialized.data) return null; + // if (!currentIsPluginsInitialized.data) return null; return (
diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index aa8c5ad7..80e7e717 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -7,6 +7,9 @@ import { useStore_IsPluginsInitialized, useStore_IsInitializedLoadPlugin, useStore_IsFetchedPluginsInfo, + + useStore_FetchedPluginsInfo, + useStore_LoadedPlugins, } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; @@ -39,6 +42,8 @@ export const usePlugins = () => { const { currentIsInitializedLoadPlugin, updateIsInitializedLoadPlugin, pendingIsInitializedLoadPlugin } = useStore_IsInitializedLoadPlugin(); const { currentIsFetchedPluginsInfo, updateIsFetchedPluginsInfo, pendingIsFetchedPluginsInfo } = useStore_IsFetchedPluginsInfo(); + const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo } = useStore_FetchedPluginsInfo(); + const { currentLoadedPlugins, updateLoadedPlugins, pendingLoadedPlugins } = useStore_LoadedPlugins(); const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); @@ -47,65 +52,24 @@ export const usePlugins = () => { const { asyncTauriFetchGithub } = useFetch(); + + 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); } - updatePluginsData(prev => { - console.log("-----updated downloaded plugin info----"); + updateLoadedPlugins(prev => { const prev_map = new Map(prev.data.map(item => [item.plugin_id, item])); - const new_data = []; - let new_value = {}; - const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(downloaded_plugin_info.min_supported_vrct_version, downloaded_plugin_info.max_supported_vrct_version); - - const target_plugin = prev_map.get(downloaded_plugin_info.plugin_id); - if (!target_plugin) { // 未ダウンロード 新規登録 - new_value = { - plugin_id: downloaded_plugin_info.plugin_id, - component: component, - is_downloaded: true, - is_latest_version_available: false, - is_latest_version_already: true, - downloaded_plugin_info: { - ...downloaded_plugin_info, - component: component, - is_plugin_supported: is_plugin_supported, - is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, - }, - }; - return [...prev.data, new_value]; - } - - for (const old_plugin_data of prev.data) { - - if (old_plugin_data.plugin_id === downloaded_plugin_info.plugin_id) { // ダウンロード済み or 登録済 アップデート - const target_prev_plugin = prev_map.get(downloaded_plugin_info.plugin_id); - const is_latest_version_available = (target_prev_plugin.is_downloaded) && (target_prev_plugin.latest_plugin_info?.plugin_version) && !(downloaded_plugin_info.plugin_version === target_prev_plugin.latest_plugin_info?.plugin_version); - - new_value = { - ...target_prev_plugin, - plugin_id: downloaded_plugin_info.plugin_id, - component: component, - is_downloaded: true, - is_latest_version_available: is_latest_version_available, - is_latest_version_already: (target_plugin.plugin_version === old_plugin_data.latest_plugin_info?.plugin_info), - downloaded_plugin_info: { - ...downloaded_plugin_info, - component: component, - is_plugin_supported: is_plugin_supported, - is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, - }, - }; - } else { - new_value = old_plugin_data; - } - new_data.push(new_value); - } - return new_data; + 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 } @@ -259,10 +223,11 @@ export const usePlugins = () => { const asyncFetchPluginsInfo = async () => { + if (currentIsFetchedPluginsInfo.data) return; try { const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL); if (response.status !== 200) { - throw new Error("Failed to fetch plugin list, status: " + response.status); + throw new Error("Failed to fetch plugins list, status: " + response.status); } const plugins_data = response.data; const updated_list = await Promise.all( @@ -271,7 +236,7 @@ export const usePlugins = () => { 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); + 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, @@ -282,13 +247,15 @@ export const usePlugins = () => { } }) ); - return updated_list; + updateFetchedPluginsInfo(updated_list); + updateIsFetchedPluginsInfo(true); } catch (error) { - console.error("Error fetching plugin info list:", error); + console.error("Error fetching plugin info list: ", error); } } 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}`); @@ -371,6 +338,12 @@ export const usePlugins = () => { currentIsFetchedPluginsInfo, updateIsFetchedPluginsInfo, + currentFetchedPluginsInfo, + updateFetchedPluginsInfo, + + currentLoadedPlugins, + updateLoadedPlugins, + setSavedPluginsStatus, handlePendingPlugin, diff --git a/src-ui/store.js b/src-ui/store.js index 58012777..3d4bac13 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -281,7 +281,9 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA export const { atomInstance: Atom_IsPluginsInitialized, useHook: useStore_IsPluginsInitialized } = createAtomWithHook(false, "IsPluginsInitialized"); export const { atomInstance: Atom_IsInitializedLoadPlugin, useHook: useStore_IsInitializedLoadPlugin } = createAtomWithHook(false, "IsInitializedLoadPlugin"); export const { atomInstance: Atom_IsFetchedPluginsInfo, useHook: useStore_IsFetchedPluginsInfo } = createAtomWithHook(false, "IsFetchedPluginsInfo"); -export const { atomInstance: Atom_FetchedPluginsInfo, useHook: useStore_FetchedPluginsInfo } = createAtomWithHook(false, "FetchedPluginsInfo"); + +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"); From ee4bbf772f4431f84370540ae049e8cb76d82904 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 17 Apr 2025 00:11:50 +0900 Subject: [PATCH 29/47] [Refactor] Remove the file and code that is no longer in use. --- .../_app_controllers/PluginsController.jsx | 2 -- .../LoadPluginsController.jsx | 2 +- .../MergePluginsController.jsx | 35 ++----------------- .../MergeSavedPluginsStatusController.jsx | 35 ------------------- 4 files changed, 4 insertions(+), 70 deletions(-) delete mode 100644 src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 9840afd5..4f47db05 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -8,7 +8,6 @@ if (typeof window !== "undefined") { import { LoadPluginsController } from "./plugins_controllers/LoadPluginsController"; import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController"; -// import { MergeSavedPluginsStatusController } from "./plugins_controllers/MergeSavedPluginsStatusController"; import { MergePluginsController } from "./plugins_controllers/MergePluginsController"; export const PluginsController = ({ pluginsControllerHasRunRef }) => { @@ -18,7 +17,6 @@ export const PluginsController = ({ pluginsControllerHasRunRef }) => { - {/* */} ); }; \ 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 index 0945df96..ee4caeac 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { usePlugins } from "@logics_configs"; export const LoadPluginsController = ({ pluginsControllerHasRunRef }) => { diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index 2fdb51c9..c4545bc3 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -121,7 +121,7 @@ export const MergePluginsController = () => { } - // 追加処理: ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化 + // ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化 new_data.forEach(plugin => { if (plugin.is_downloaded && plugin.is_enabled) { if ( @@ -143,7 +143,7 @@ export const MergePluginsController = () => { - // --- 自動アップデート(ダウンロード処理)のuseEffect --- + // --- 自動アップデート(ダウンロード処理)--- // ※downloadAndExtractPlugin の重複実行を防ぐため、実行中の plugin_id を useRef で管理 const downloadingRef = useRef(new Set()); @@ -160,7 +160,6 @@ export const MergePluginsController = () => { if (!downloadingRef.current.has(plugin.plugin_id)) { downloadingRef.current.add(plugin.plugin_id); - // ※ downloadAndExtractPlugin は外部通信を伴い currentLoadedPlugins.data 更新を引き起こすので注意 downloadAndExtractPlugin(plugin) .then(() => { console.log(`Plugin ${plugin.plugin_id} updated successfully`); @@ -175,33 +174,5 @@ export const MergePluginsController = () => { }); }, [currentPluginsData.data]); - - - return null; -}; - - - - - -// ダウンロード済みで最新版じゃない場合、自動的にアップデート -// // is_latest_version_supported: true のみ。 -// // 失敗した場合、現在のバージョンが非対応の場合はdisabledにする。 -// new_data.forEach(async plugin => { -// if (plugin.is_enabled) { -// console.log(plugin); - -// if (!plugin.is_latest_version_already && plugin.is_latest_version_available) { -// await downloadAndExtractPlugin(plugin); -// } -// } -// }); - -// new_data.forEach(plugin => { -// if (plugin.is_downloaded && plugin.is_enabled) { -// if (!plugin.downloaded_plugin_info?.is_plugin_supported && !plugin.latest_plugin_info?.is_plugin_supported) { -// plugin.is_enabled = false -// } -// } -// }); \ No newline at end of file +}; \ No newline at end of file diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx deleted file mode 100644 index 9b3dcb23..00000000 --- a/src-ui/app/_app_controllers/plugins_controllers/MergeSavedPluginsStatusController.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect } from "react"; -import { usePlugins } from "@logics_configs"; - -export const MergeSavedPluginsStatusController = () => { - const { - updatePluginsData, - currentSavedPluginsStatus, - } = usePlugins(); - - useEffect(() => { - updatePluginsData(prev => { - // currentSavedPluginsStatus.data の各要素を Map 化して plugin_id で参照 - 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])); - // prev.data にある各アイテムについて、保存済みの状態情報があればマージ - const merged = prev.data.map(item => { - - if (saved_map.has(item.plugin_id)) { - return { ...item, is_enabled: saved_map.get(item.plugin_id).is_enabled }; - } - return item; - }); - - // currentSavedPluginsStatus.data にのみ存在する項目があれば追加 - currentSavedPluginsStatus.data.forEach(saved => { - if (!prev_map.has(saved.plugin_id)) { - merged.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled }); - } - }); - return merged; - }); - }, [currentSavedPluginsStatus]); - - return null; -}; \ No newline at end of file From 1c56167dbadde88ba45fa1ceab125d0015bd4462 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:30:30 +0900 Subject: [PATCH 30/47] [Update] Plugins: Show enabled plugins compatibility when update modal is opened. --- .../update_modal/UpdateModal.jsx | 67 +++++++------------ .../update_modal/UpdateModal.module.scss | 10 ++- .../PluginCompatibilityList.jsx | 61 +++++++++++++++++ .../PluginCompatibilityList.module.scss | 63 +++++++++++++++++ src-ui/assets/x_mark.svg | 1 + 5 files changed, 160 insertions(+), 42 deletions(-) create mode 100644 src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.jsx create mode 100644 src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.module.scss create mode 100644 src-ui/assets/x_mark.svg diff --git a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx index caf89d43..c35a240c 100644 --- a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx +++ b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import styles from "./UpdateModal.module.scss"; import { useTranslation } from "react-i18next"; import { useStore_OpenedQuickSetting } from "@store"; @@ -9,7 +10,7 @@ import { useSoftwareVersion, } from "@logics_common"; -import clsx from "clsx"; +import { PluginCompatibilityList } from "./plugins_compatibility_list/PluginCompatibilityList"; export const UpdateModal = () => { const { t } = useTranslation(); @@ -44,31 +45,34 @@ export const UpdateModal = () => { return (
- {isAnyPluginEnabled() && } -
-
-
- - {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")}

+
+
@@ -93,23 +97,4 @@ const CurrentVersionLabel = (props) => { return

{t("update_modal.is_latest_version_already")}

; } return

{t("update_modal.is_current_compute_device")}

; -}; - -const PluginUpdateNotification = () => { - const { enabledPluginsList } = usePlugins(); - - // ダウンロード済みのもの or プラグイン最新版が、VRCT最新版(VRCTアプデ後)に非対応のもの - const incompatible_plugins_list = enabledPluginsList().filter(plugin => { - if (!plugin.is_downloaded) return false; - if (!plugin.downloaded_plugin_info?.is_plugin_supported_latest_vrct || !plugin.latest_plugin_info.is_plugin_supported_latest_vrct) return true; - }); - - return ( -
- {incompatible_plugins_list.map(plugin => { - const target_data = plugin.downloaded_plugin_info; - return

{target_data.title}

- })} -
- ); }; \ No newline at end of file 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..96e1054a --- /dev/null +++ b/src-ui/app/modal_controller/update_modal/plugins_compatibility_list/PluginCompatibilityList.jsx @@ -0,0 +1,61 @@ +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 } = usePlugins(); + + // ダウンロード済みのもの + const downloaded_plugin = enabledPluginsList().filter(p => p.is_downloaded); + + // プラグイン最新版が、VRCT最新版(VRCTアプデ後)に非対応のもの + 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) { + incompatible_plugins_list.push(p); + } else { + compatible_plugins_list.push(p); + } + } + + const is_any_compatible_plugin = incompatible_plugins_list.length > 0; + + return ( +
+

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

+
+ {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_compatible_plugin && +
+ +

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

+
+ } +
+ ); +}; + +const PluginContainer = ({ target_data, is_compatible }) => { + console.log(target_data.plugin_id); + + 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/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 From b8c1688172c22f75773ac601e8e8b3a2f730c521 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:46:17 +0900 Subject: [PATCH 31/47] [Update] Plugins: separate dev mode. url and import path. --- .../PluginCompatibilityList.jsx | 2 -- src-ui/logics/configs/plugins/usePlugins.js | 4 ++-- src-ui/ui_configs.js | 15 ++++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) 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 index 96e1054a..3591f962 100644 --- 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 @@ -47,8 +47,6 @@ export const PluginCompatibilityList = () => { }; const PluginContainer = ({ target_data, is_compatible }) => { - console.log(target_data.plugin_id); - return (

{target_data.title}

diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 80e7e717..2e92ef74 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/tauri"; -import { IS_PLUGIN_DEV_MODE, getPluginsList } from "@ui_configs"; +import { IS_PLUGIN_PATH_DEV_MODE, getPluginsList } from "@ui_configs"; import { createAtomWithHook, useStore_SavedPluginsStatus, @@ -110,7 +110,7 @@ export const usePlugins = () => { }; const asyncLoadAllPlugins = async () => { - if (IS_PLUGIN_DEV_MODE) { + 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); diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index 6db3193f..6b8d0031 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -52,15 +52,20 @@ export const ui_configs = { ] }; -export const IS_PLUGIN_DEV_MODE = false; -if (IS_PLUGIN_DEV_MODE) console.warn("ui_configs IS_PLUGIN_DEV_MODE: true. Turn to 'false' when it's production environment."); +// 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_DEV_MODE) - ? base_url + "dev_vrct_plugins_list.json" - : base_url + "vrct_plugins_list.json"; + 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 }, From fd59c0b28ff9aaace218010dc025ba9a1047cd9b Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 17 Apr 2025 19:44:14 +0900 Subject: [PATCH 32/47] [Update/Refactor] Automatically disable and save the status to config.json if the plugin is not supported. --- .../MergePluginsController.jsx | 2 ++ .../setting_box/plugins/Plugins.jsx | 31 ++----------------- src-ui/logics/configs/plugins/usePlugins.js | 29 +++++++++++++++++ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index c4545bc3..e0e258b7 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -10,6 +10,7 @@ export const MergePluginsController = () => { currentFetchedPluginsInfo, currentSavedPluginsStatus, downloadAndExtractPlugin, + toggleSavedPluginStatus, } = usePlugins(); const { checkVrctVerCompatibility } = useSoftwareVersion(); @@ -129,6 +130,7 @@ export const MergePluginsController = () => { !plugin.latest_plugin_info?.is_plugin_supported ) { plugin.is_enabled = false; + toggleSavedPluginStatus(plugin.plugin_id); } } }); diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 9f7f490e..e60927f4 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -22,9 +22,8 @@ const PluginDownloadContainer = () => { const { downloadAndExtractPlugin, currentPluginsData, - updatePluginsData, currentSavedPluginsStatus, - setSavedPluginsStatus, + toggleSavedPluginStatus, handlePendingPlugin, } = usePlugins(); @@ -42,33 +41,7 @@ const PluginDownloadContainer = () => { // プラグインのオンオフ切り替え処理 const toggleFunction = (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; - } - return d; - }); - } else { - new_value.push(...currentSavedPluginsStatus.data); - new_value.push({ - plugin_id: target_plugin_id, - is_enabled: true, - }); - } - - // 「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); + toggleSavedPluginStatus(target_plugin_id); }; const variable_state = currentSavedPluginsStatus.state; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 2e92ef74..53db0c0b 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -298,7 +298,35 @@ export const usePlugins = () => { }); }; + const toggleSavedPluginStatus = (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; + } + return d; + }); + } else { + new_value.push(...currentSavedPluginsStatus.data); + new_value.push({ + plugin_id: target_plugin_id, + is_enabled: true, + }); + } + // 「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); + }; const setSavedPluginsStatus = (plugins_status) => { pendingSavedPluginsStatus(); @@ -344,6 +372,7 @@ export const usePlugins = () => { currentLoadedPlugins, updateLoadedPlugins, + toggleSavedPluginStatus, setSavedPluginsStatus, handlePendingPlugin, From 9ef56db2dd70d000b256ab410e45edfb9ee176f4 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:10:09 +0900 Subject: [PATCH 33/47] [Update] Add safety that prevent to show the title Plugin compatibility list unnecessary. Remove the test code that is for showing the update modal. --- .../PluginCompatibilityList.jsx | 10 +++++++--- src-ui/logics/useReceiveRoutes.js | 8 +------- 2 files changed, 8 insertions(+), 10 deletions(-) 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 index 3591f962..d460b104 100644 --- 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 @@ -10,18 +10,22 @@ export const PluginCompatibilityList = () => { // ダウンロード済みのもの const downloaded_plugin = enabledPluginsList().filter(p => p.is_downloaded); - // プラグイン最新版が、VRCT最新版(VRCTアプデ後)に非対応のもの 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_compatible_plugin = incompatible_plugins_list.length > 0; + 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. return (
@@ -36,7 +40,7 @@ export const PluginCompatibilityList = () => { return ; })}
- {is_any_compatible_plugin && + {is_any_incompatible_plugin &&

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

diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index fb826f8c..b73fe62e 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -206,13 +206,7 @@ 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/software_update_info": () => { - updateLatestSoftwareVersionInfo({ - is_update_available: true, - new_version: "3.0.3", - }) - }, - // "/run/software_update_info": updateLatestSoftwareVersionInfo, + "/run/software_update_info": updateLatestSoftwareVersionInfo, "/run/connected_network": handleNetworkConnection, // Main Page From 01b7d6c94581d647b2b6c6827907957c8700222b Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:38:31 +0900 Subject: [PATCH 34/47] [Update] Plugins: Sort plugins list and remain the order of it. --- .../setting_section/setting_box/plugins/Plugins.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index e60927f4..2a02a4b0 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -17,7 +17,6 @@ export const Plugins = () => { ); }; - const PluginDownloadContainer = () => { const { downloadAndExtractPlugin, @@ -46,9 +45,14 @@ const PluginDownloadContainer = () => { const variable_state = currentSavedPluginsStatus.state; + // plugin_id で ABC 順にソート + const sorted_plugins_data = [...currentPluginsData.data].sort((a, b) => + a.plugin_id.localeCompare(b.plugin_id) + ); + return (
- {currentPluginsData.data.map((plugin) => ( + {sorted_plugins_data.map((plugin) => (

{plugin.is_downloaded @@ -79,4 +83,4 @@ const PluginDownloadContainer = () => { ))}

); -}; +}; \ No newline at end of file From 0be9674f69a4608fa03d6b2311870dc04714a2a0 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 18 Apr 2025 01:50:30 +0900 Subject: [PATCH 35/47] [bugfix] Plugins: Fix the error that was happened if 'plugins' directory is not exist and try to load. --- src-ui/logics/configs/plugins/usePlugins.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 53db0c0b..32149def 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -124,6 +124,9 @@ export const usePlugins = () => { } }); } 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)); From 74e38f931822ad7c5cac4befdc59744e96959c00 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:30:47 +0900 Subject: [PATCH 36/47] [bugfix] Fix the bug that the plugin update unintentionally when its latest version available and it turn to enabled. --- .../plugins_controllers/MergePluginsController.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index e0e258b7..73b6f615 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -126,7 +126,8 @@ export const MergePluginsController = () => { new_data.forEach(plugin => { if (plugin.is_downloaded && plugin.is_enabled) { if ( - !plugin.downloaded_plugin_info?.is_plugin_supported && + !plugin.downloaded_plugin_info.is_plugin_supported && + plugin.latest_plugin_info && !plugin.latest_plugin_info?.is_plugin_supported ) { plugin.is_enabled = false; @@ -155,6 +156,7 @@ export const MergePluginsController = () => { 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 ) { From 7bcbefaf0697bd15f466569a56d0e3dd0cfc4537 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:53:36 +0900 Subject: [PATCH 37/47] [Refactor] Remove the code that is no longer in use. --- .../plugins_controllers/LoadPluginsController.jsx | 5 +---- .../plugins_controllers/MergePluginsController.jsx | 4 ++-- .../setting_section/setting_box/plugins/Plugins.jsx | 7 ++----- src-ui/logics/configs/plugins/usePlugins.js | 13 ++----------- src-ui/store.js | 2 -- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx index ee4caeac..c9c953da 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx @@ -4,7 +4,6 @@ import { usePlugins } from "@logics_configs"; export const LoadPluginsController = ({ pluginsControllerHasRunRef }) => { const { asyncLoadAllPlugins, - updateIsInitializedLoadPlugin, } = usePlugins(); const asyncInitLoadPlugins = async () => { @@ -17,9 +16,7 @@ export const LoadPluginsController = ({ pluginsControllerHasRunRef }) => { useEffect(() => { if (!pluginsControllerHasRunRef.current.is_initialized_load_plugin) { - asyncInitLoadPlugins().then(() => { - updateIsInitializedLoadPlugin(true); - }); + asyncInitLoadPlugins(); pluginsControllerHasRunRef.current.is_initialized_load_plugin = true; } }, []); diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index 73b6f615..85df42d1 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -10,7 +10,7 @@ export const MergePluginsController = () => { currentFetchedPluginsInfo, currentSavedPluginsStatus, downloadAndExtractPlugin, - toggleSavedPluginStatus, + toggleSavedPluginsStatus, } = usePlugins(); const { checkVrctVerCompatibility } = useSoftwareVersion(); @@ -131,7 +131,7 @@ export const MergePluginsController = () => { !plugin.latest_plugin_info?.is_plugin_supported ) { plugin.is_enabled = false; - toggleSavedPluginStatus(plugin.plugin_id); + toggleSavedPluginsStatus(plugin.plugin_id); } } }); diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 2a02a4b0..46661133 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -5,11 +5,8 @@ import { PluginsControlComponent } from "../_components/plugins_control_componen export const Plugins = () => { const { - currentIsPluginsInitialized, } = usePlugins(); - // if (!currentIsPluginsInitialized.data) return null; - return (
@@ -22,7 +19,7 @@ const PluginDownloadContainer = () => { downloadAndExtractPlugin, currentPluginsData, currentSavedPluginsStatus, - toggleSavedPluginStatus, + toggleSavedPluginsStatus, handlePendingPlugin, } = usePlugins(); @@ -40,7 +37,7 @@ const PluginDownloadContainer = () => { // プラグインのオンオフ切り替え処理 const toggleFunction = (target_plugin_id) => { - toggleSavedPluginStatus(target_plugin_id); + toggleSavedPluginsStatus(target_plugin_id); }; const variable_state = currentSavedPluginsStatus.state; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 32149def..813bf0f7 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -4,8 +4,6 @@ import { createAtomWithHook, useStore_SavedPluginsStatus, useStore_PluginsData, - useStore_IsPluginsInitialized, - useStore_IsInitializedLoadPlugin, useStore_IsFetchedPluginsInfo, useStore_FetchedPluginsInfo, @@ -39,7 +37,6 @@ const PLUGIN_LIST_URL = getPluginsList(); export const usePlugins = () => { const { asyncStdoutToPython } = useStdoutToPython(); - const { currentIsInitializedLoadPlugin, updateIsInitializedLoadPlugin, pendingIsInitializedLoadPlugin } = useStore_IsInitializedLoadPlugin(); const { currentIsFetchedPluginsInfo, updateIsFetchedPluginsInfo, pendingIsFetchedPluginsInfo } = useStore_IsFetchedPluginsInfo(); const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo } = useStore_FetchedPluginsInfo(); @@ -47,7 +44,6 @@ export const usePlugins = () => { const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData(); - const { currentIsPluginsInitialized, updateIsPluginsInitialized, pendingIsPluginsInitialized } = useStore_IsPluginsInitialized(); const { checkVrctVerCompatibility } = useSoftwareVersion(); const { asyncTauriFetchGithub } = useFetch(); @@ -301,7 +297,7 @@ export const usePlugins = () => { }); }; - const toggleSavedPluginStatus = (target_plugin_id) => { + const toggleSavedPluginsStatus = (target_plugin_id) => { const is_exists = currentSavedPluginsStatus.data.some( (d) => d.plugin_id === target_plugin_id ); @@ -361,11 +357,6 @@ export const usePlugins = () => { currentPluginsData, updatePluginsData, - currentIsPluginsInitialized, - updateIsPluginsInitialized, - - currentIsInitializedLoadPlugin, - updateIsInitializedLoadPlugin, currentIsFetchedPluginsInfo, updateIsFetchedPluginsInfo, @@ -375,7 +366,7 @@ export const usePlugins = () => { currentLoadedPlugins, updateLoadedPlugins, - toggleSavedPluginStatus, + toggleSavedPluginsStatus, setSavedPluginsStatus, handlePendingPlugin, diff --git a/src-ui/store.js b/src-ui/store.js index 3d4bac13..260285c7 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -278,8 +278,6 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA }, "Hotkeys"); // Plugins -export const { atomInstance: Atom_IsPluginsInitialized, useHook: useStore_IsPluginsInitialized } = createAtomWithHook(false, "IsPluginsInitialized"); -export const { atomInstance: Atom_IsInitializedLoadPlugin, useHook: useStore_IsInitializedLoadPlugin } = createAtomWithHook(false, "IsInitializedLoadPlugin"); export const { atomInstance: Atom_IsFetchedPluginsInfo, useHook: useStore_IsFetchedPluginsInfo } = createAtomWithHook(false, "IsFetchedPluginsInfo"); export const { atomInstance: Atom_FetchedPluginsInfo, useHook: useStore_FetchedPluginsInfo } = createAtomWithHook([], "FetchedPluginsInfo"); From cd63e2e3ca005211441aa26512be90edc0f587fd Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 19 Apr 2025 16:15:47 +0900 Subject: [PATCH 38/47] [bugfix/Refactor] Fix plugin compatibility list that was showed incorrectly. Change init function. --- src-ui/app/App.jsx | 11 ++--- .../_app_controllers/PluginsController.jsx | 6 +-- .../FetchLatestPluginsDataController.jsx | 16 ++----- .../LoadPluginsController.jsx | 7 +-- .../MergePluginsController.jsx | 12 ++++- .../setting_box/plugins/Plugins.jsx | 11 ++++- .../PluginCompatibilityList.jsx | 12 ++++- src-ui/logics/configs/plugins/usePlugins.js | 47 +++++++++++++++---- src-ui/store.js | 4 +- 9 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src-ui/app/App.jsx b/src-ui/app/App.jsx index 9c7e8aa0..c2b0e4f3 100644 --- a/src-ui/app/App.jsx +++ b/src-ui/app/App.jsx @@ -1,4 +1,3 @@ -import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { @@ -29,10 +28,6 @@ export const App = () => { const { currentIsBackendReady } = useIsBackendReady(); const { WindowGeometryController } = useWindow(); const { i18n } = useTranslation(); - const pluginsControllerHasRunRef = useRef({ - is_initialized_load_plugin: false, - is_init_fetched_plugins_info: false, - }); return (
@@ -49,7 +44,7 @@ export const App = () => { {(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false) ? - : + : } @@ -58,11 +53,11 @@ export const App = () => { ); }; -const Contents = ({ pluginsControllerHasRunRef }) => { +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 index 4f47db05..0abeb5cb 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -10,13 +10,13 @@ import { LoadPluginsController } from "./plugins_controllers/LoadPluginsControll import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController"; import { MergePluginsController } from "./plugins_controllers/MergePluginsController"; -export const PluginsController = ({ pluginsControllerHasRunRef }) => { +export const PluginsController = () => { return ( <> - - + + ); }; \ 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 index 088d9cae..1f9d5e75 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/FetchLatestPluginsDataController.jsx @@ -1,23 +1,15 @@ import { useEffect } from "react"; import { usePlugins } from "@logics_configs"; -export const FetchLatestPluginsDataController = ({ pluginsControllerHasRunRef }) => { +export const FetchLatestPluginsDataController = () => { const { asyncFetchPluginsInfo, + isAnyPluginEnabled_Init, } = usePlugins(); - const asyncInitFetchPluginsInfo = async () => { - try { - await asyncFetchPluginsInfo(); - } catch (error) { - console.error(error); - } - }; - useEffect(() => { - if (!pluginsControllerHasRunRef.current.is_init_fetched_plugins_info) { - asyncInitFetchPluginsInfo(); - pluginsControllerHasRunRef.current.is_init_fetched_plugins_info = true; + if (isAnyPluginEnabled_Init()) { + asyncFetchPluginsInfo(); } }, []); diff --git a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx index c9c953da..46ad274b 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/LoadPluginsController.jsx @@ -1,7 +1,8 @@ import { useEffect } from "react"; import { usePlugins } from "@logics_configs"; +import { store } from "@store"; -export const LoadPluginsController = ({ pluginsControllerHasRunRef }) => { +export const LoadPluginsController = () => { const { asyncLoadAllPlugins, } = usePlugins(); @@ -15,9 +16,9 @@ export const LoadPluginsController = ({ pluginsControllerHasRunRef }) => { }; useEffect(() => { - if (!pluginsControllerHasRunRef.current.is_initialized_load_plugin) { + if (!store.is_initialized_load_plugin) { asyncInitLoadPlugins(); - pluginsControllerHasRunRef.current.is_initialized_load_plugin = true; + store.is_initialized_load_plugin = true; } }, []); diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index 85df42d1..9c8e02ca 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -10,7 +10,7 @@ export const MergePluginsController = () => { currentFetchedPluginsInfo, currentSavedPluginsStatus, downloadAndExtractPlugin, - toggleSavedPluginsStatus, + setTargetSavedPluginsStatus_Init, } = usePlugins(); const { checkVrctVerCompatibility } = useSoftwareVersion(); @@ -131,7 +131,15 @@ export const MergePluginsController = () => { !plugin.latest_plugin_info?.is_plugin_supported ) { plugin.is_enabled = false; - toggleSavedPluginsStatus(plugin.plugin_id); + setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); + } + + if ( + !plugin.downloaded_plugin_info.is_plugin_supported && + plugin.is_outdated + ) { + plugin.is_enabled = false; + setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); } } }); diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 46661133..5f6df29e 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -1,11 +1,20 @@ -import React from "react"; +import { useEffect, useRef } from "react"; import { usePlugins } from "@logics_configs"; import styles from "./Plugins.module.scss"; import { PluginsControlComponent } from "../_components/plugins_control_component/PluginsControlComponent"; export const Plugins = () => { const { + asyncFetchPluginsInfo, } = usePlugins(); + const hasRunRef = useRef(false); + + useEffect(() => { + if (!hasRunRef.current) { + asyncFetchPluginsInfo(); + } + return () => hasRunRef.current = true; + }, []); return (
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 index d460b104..dd0133b1 100644 --- 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 @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import styles from "./PluginCompatibilityList.module.scss"; import { usePlugins } from "@logics_configs"; import CheckMarkSvg from "@images/check_mark.svg?react"; @@ -5,7 +6,14 @@ import XSvg from "@images/x_mark.svg?react"; import WarningSvg from "@images/warning.svg?react"; export const PluginCompatibilityList = () => { - const { enabledPluginsList } = usePlugins(); + const { + enabledPluginsList, + asyncFetchPluginsInfo, + } = usePlugins(); + + useEffect(() => { + asyncFetchPluginsInfo(); + }, []); // ダウンロード済みのもの const downloaded_plugin = enabledPluginsList().filter(p => p.is_downloaded); @@ -13,7 +21,7 @@ export const PluginCompatibilityList = () => { 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) { + 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 { diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 813bf0f7..0a3e65f1 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,10 +1,11 @@ import { invoke } from "@tauri-apps/api/tauri"; import { IS_PLUGIN_PATH_DEV_MODE, getPluginsList } from "@ui_configs"; import { + store, + createAtomWithHook, useStore_SavedPluginsStatus, useStore_PluginsData, - useStore_IsFetchedPluginsInfo, useStore_FetchedPluginsInfo, useStore_LoadedPlugins, @@ -37,8 +38,6 @@ const PLUGIN_LIST_URL = getPluginsList(); export const usePlugins = () => { const { asyncStdoutToPython } = useStdoutToPython(); - const { currentIsFetchedPluginsInfo, updateIsFetchedPluginsInfo, pendingIsFetchedPluginsInfo } = useStore_IsFetchedPluginsInfo(); - const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo } = useStore_FetchedPluginsInfo(); const { currentLoadedPlugins, updateLoadedPlugins, pendingLoadedPlugins } = useStore_LoadedPlugins(); @@ -222,7 +221,9 @@ export const usePlugins = () => { const asyncFetchPluginsInfo = async () => { - if (currentIsFetchedPluginsInfo.data) return; + if (store.is_fetched_plugins_info) return; + store.is_fetched_plugins_info = true; + try { const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL); if (response.status !== 200) { @@ -247,7 +248,6 @@ export const usePlugins = () => { }) ); updateFetchedPluginsInfo(updated_list); - updateIsFetchedPluginsInfo(true); } catch (error) { console.error("Error fetching plugin info list: ", error); } @@ -327,11 +327,43 @@ export const usePlugins = () => { 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 = () => { + console.log(currentSavedPluginsStatus); + return currentSavedPluginsStatus.data.some(plugin => plugin.is_enabled); + }; + const isAnyPluginEnabled = () => { return currentPluginsData.data.some(plugin => plugin.is_enabled); }; @@ -345,6 +377,7 @@ export const usePlugins = () => { return { asyncFetchPluginsInfo, + isAnyPluginEnabled_Init, isAnyPluginEnabled, enabledPluginsList, @@ -357,9 +390,6 @@ export const usePlugins = () => { currentPluginsData, updatePluginsData, - currentIsFetchedPluginsInfo, - updateIsFetchedPluginsInfo, - currentFetchedPluginsInfo, updateFetchedPluginsInfo, @@ -367,6 +397,7 @@ export const usePlugins = () => { updateLoadedPlugins, toggleSavedPluginsStatus, + setTargetSavedPluginsStatus_Init, setSavedPluginsStatus, handlePendingPlugin, diff --git a/src-ui/store.js b/src-ui/store.js index 260285c7..f99ed73b 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -20,6 +20,8 @@ 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: false, }; const generatePropertyNames = (base_name) => ({ @@ -278,8 +280,6 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA }, "Hotkeys"); // Plugins -export const { atomInstance: Atom_IsFetchedPluginsInfo, useHook: useStore_IsFetchedPluginsInfo } = createAtomWithHook(false, "IsFetchedPluginsInfo"); - 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"); From 33f1e54f4d17a7c67840f0b12710f707433605d8 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 19 Apr 2025 22:09:20 +0900 Subject: [PATCH 39/47] [bugfix] Plugins: fix auto disable plugins function. --- .../MergePluginsController.jsx | 56 ++++++++++--------- src-ui/logics/configs/plugins/usePlugins.js | 7 ++- src-ui/store.js | 3 +- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index 9c8e02ca..21a892a3 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -1,4 +1,5 @@ import { useEffect, useRef } from "react"; +import { store } from "@store"; import { usePlugins } from "@logics_configs"; import { useSoftwareVersion } from "@logics_common"; @@ -121,29 +122,6 @@ export const MergePluginsController = () => { } } - - // ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化 - new_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 - ) { - plugin.is_enabled = false; - setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); - } - - if ( - !plugin.downloaded_plugin_info.is_plugin_supported && - plugin.is_outdated - ) { - plugin.is_enabled = false; - setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); - } - } - }); - console.log("merged plugin data", new_data); return new_data; }); @@ -168,8 +146,6 @@ export const MergePluginsController = () => { !plugin.is_latest_version_already && plugin.is_latest_version_available ) { - console.log(!downloadingRef.current.has(plugin.plugin_id)); - if (!downloadingRef.current.has(plugin.plugin_id)) { downloadingRef.current.add(plugin.plugin_id); downloadAndExtractPlugin(plugin) @@ -186,5 +162,35 @@ export const MergePluginsController = () => { }); }, [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 + ) { + plugin.is_enabled = false; + setTargetSavedPluginsStatus_Init(plugin.plugin_id, false); + } + + if ( + !plugin.downloaded_plugin_info.is_plugin_supported && + plugin.is_outdated + ) { + 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/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 0a3e65f1..2fd911b5 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -221,8 +221,8 @@ export const usePlugins = () => { const asyncFetchPluginsInfo = async () => { - if (store.is_fetched_plugins_info) return; - store.is_fetched_plugins_info = true; + if (store.is_fetched_plugins_info_already) return; + store.is_fetched_plugins_info_already = true; try { const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL); @@ -251,6 +251,8 @@ export const usePlugins = () => { } catch (error) { console.error("Error fetching plugin info list: ", error); } + + store.is_initialized_fetched_plugin_info = true; } const asyncFetchPluginInfo = async (plugin_info_asset_url) => { @@ -360,7 +362,6 @@ export const usePlugins = () => { // init時、currentPluginsDataからのデータではデータ更新が間に合わないので、currentSavedPluginsStatusから直接取得 const isAnyPluginEnabled_Init = () => { - console.log(currentSavedPluginsStatus); return currentSavedPluginsStatus.data.some(plugin => plugin.is_enabled); }; diff --git a/src-ui/store.js b/src-ui/store.js index f99ed73b..c181ced5 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -21,7 +21,8 @@ export const store = { text_area_ref: null, is_applied_init_message_box_height: false, is_initialized_load_plugin: false, - is_fetched_plugins_info: false, + is_fetched_plugins_info_already: false, + is_initialized_fetched_plugin_info: false, }; const generatePropertyNames = (base_name) => ({ From b9d9edb7c352010eb10e2d92c3ddf082e523acea Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:48:05 +0900 Subject: [PATCH 40/47] [Update] Add the safety that is not show on UI if the plugin, downloaded, is not supported. --- .../plugins_controllers/MergePluginsController.jsx | 10 ++++++---- src-ui/app/main_page/main_section/MainSection.jsx | 7 ++++++- .../main_page/main_section/MainSection.module.scss | 1 + src-ui/logics/configs/plugins/usePlugins.js | 13 +++++++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index 21a892a3..e8175c43 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -7,6 +7,7 @@ export const MergePluginsController = () => { const { currentLoadedPlugins, updatePluginsData, + updateTargetPluginData, currentPluginsData, currentFetchedPluginsInfo, currentSavedPluginsStatus, @@ -148,14 +149,15 @@ export const MergePluginsController = () => { ) { if (!downloadingRef.current.has(plugin.plugin_id)) { downloadingRef.current.add(plugin.plugin_id); + const target_plugin_id = plugin.plugin_id; downloadAndExtractPlugin(plugin) .then(() => { - console.log(`Plugin ${plugin.plugin_id} updated successfully`); - downloadingRef.current.delete(plugin.plugin_id); + console.log(`Plugin ${target_plugin_id} updated successfully`); + downloadingRef.current.delete(target_plugin_id); }) .catch((error) => { - console.error(`Plugin ${plugin.plugin_id} update failed`, error); - downloadingRef.current.delete(plugin.plugin_id); + console.error(`Plugin ${target_plugin_id} update failed`, error); + downloadingRef.current.delete(target_plugin_id); }); } } diff --git a/src-ui/app/main_page/main_section/MainSection.jsx b/src-ui/app/main_page/main_section/MainSection.jsx index e89071a8..b0ffb77c 100644 --- a/src-ui/app/main_page/main_section/MainSection.jsx +++ b/src-ui/app/main_page/main_section/MainSection.jsx @@ -16,7 +16,12 @@ 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.location === "main_section")); + 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 (
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 0f592a3a..c132e869 100644 --- a/src-ui/app/main_page/main_section/MainSection.module.scss +++ b/src-ui/app/main_page/main_section/MainSection.module.scss @@ -5,6 +5,7 @@ display: flex; flex-direction: column; // justify-content: space-between; + overflow-y: auto; } .language_selector_container { diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 2fd911b5..8b9dd142 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -373,6 +373,17 @@ export const usePlugins = () => { 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 { @@ -391,6 +402,8 @@ export const usePlugins = () => { currentPluginsData, updatePluginsData, + updateTargetPluginData, + currentFetchedPluginsInfo, updateFetchedPluginsInfo, From 81a422b07d0bffd83427b099ca4b1c1f487db285 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:44:11 +0900 Subject: [PATCH 41/47] [Update] Plugins: Add notifications. --- locales/en.yml | 16 +++++++++++++++- .../MergePluginsController.jsx | 10 +++++++++- .../setting_box/plugins/Plugins.jsx | 9 +++++++++ src-ui/logics/configs/plugins/usePlugins.js | 10 ++++++++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index a52ac107..a72200cb 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -243,4 +243,18 @@ 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" + +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/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx index e8175c43..255ede1c 100644 --- a/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx +++ b/src-ui/app/_app_controllers/plugins_controllers/MergePluginsController.jsx @@ -1,13 +1,15 @@ 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, - updateTargetPluginData, currentPluginsData, currentFetchedPluginsInfo, currentSavedPluginsStatus, @@ -15,6 +17,7 @@ export const MergePluginsController = () => { setTargetSavedPluginsStatus_Init, } = usePlugins(); const { checkVrctVerCompatibility } = useSoftwareVersion(); + const { showNotification_Success, showNotification_Error } = useNotificationStatus(); // downloaded, fetched, saved の各情報をまとめてマージ useEffect(() => { @@ -148,16 +151,19 @@ export const MergePluginsController = () => { 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")); }); } } @@ -176,6 +182,7 @@ export const MergePluginsController = () => { 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); } @@ -184,6 +191,7 @@ export const MergePluginsController = () => { !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); } diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index 5f6df29e..aadaf64f 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -1,7 +1,9 @@ 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 { @@ -24,6 +26,7 @@ export const Plugins = () => { }; const PluginDownloadContainer = () => { + const { t } = useTranslation(); const { downloadAndExtractPlugin, currentPluginsData, @@ -31,16 +34,22 @@ const PluginDownloadContainer = () => { toggleSavedPluginsStatus, handlePendingPlugin, } = 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")); }); }; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 8b9dd142..b3b1e561 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -1,4 +1,5 @@ import { invoke } from "@tauri-apps/api/tauri"; +import { useTranslation } from "react-i18next"; import { IS_PLUGIN_PATH_DEV_MODE, getPluginsList } from "@ui_configs"; import { store, @@ -25,17 +26,18 @@ dev_plugins.forEach(async ({entry_path}) => { import JSZip from "jszip"; -import { useFetch, useSoftwareVersion } from "@logics_common"; +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 } = useStore_FetchedPluginsInfo(); @@ -308,6 +310,9 @@ export const usePlugins = () => { 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; }); @@ -317,6 +322,7 @@ export const usePlugins = () => { plugin_id: target_plugin_id, is_enabled: true, }); + showNotification_Success(t("plugin_notifications.is_enabled")) } // 「currentPluginsData.data」でis_downloadedがtrueのものだけ残す From 249c686442220e86c5a6e4505cbf25600718ca82 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:51:16 +0900 Subject: [PATCH 42/47] [Update] Plugins: Add error handlings when network is not connected and failed to fetch plugins info. Also add pending ui(TMP) while fetching. --- .../setting_section/setting_box/plugins/Plugins.jsx | 7 +++++++ .../plugins_compatibility_list/PluginCompatibilityList.jsx | 7 +++++++ src-ui/logics/configs/plugins/usePlugins.js | 3 ++- src-ui/logics/useReceiveRoutes.js | 7 ++++++- src-ui/store.js | 2 +- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index aadaf64f..b42ea31b 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -33,6 +33,7 @@ const PluginDownloadContainer = () => { currentSavedPluginsStatus, toggleSavedPluginsStatus, handlePendingPlugin, + currentFetchedPluginsInfo, } = usePlugins(); const { showNotification_Success, showNotification_Error } = useNotificationStatus(); @@ -65,8 +66,14 @@ const PluginDownloadContainer = () => { 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) => (

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 index dd0133b1..e23c320c 100644 --- 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 @@ -9,6 +9,7 @@ export const PluginCompatibilityList = () => { const { enabledPluginsList, asyncFetchPluginsInfo, + currentFetchedPluginsInfo, } = usePlugins(); useEffect(() => { @@ -35,9 +36,15 @@ export const PluginCompatibilityList = () => { 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; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index b3b1e561..309c012c 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -40,7 +40,7 @@ export const usePlugins = () => { const { showNotification_Success, showNotification_Error } = useNotificationStatus(); const { asyncStdoutToPython } = useStdoutToPython(); - const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo } = useStore_FetchedPluginsInfo(); + const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo, errorFetchedPluginsInfo } = useStore_FetchedPluginsInfo(); const { currentLoadedPlugins, updateLoadedPlugins, pendingLoadedPlugins } = useStore_LoadedPlugins(); const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus(); @@ -252,6 +252,7 @@ export const usePlugins = () => { updateFetchedPluginsInfo(updated_list); } catch (error) { console.error("Error fetching plugin info list: ", error); + errorFetchedPluginsInfo(); } store.is_initialized_fetched_plugin_info = true; diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index c015d43e..4a4f944c 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -206,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/software_update_info": updateLatestSoftwareVersionInfo, + "/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 diff --git a/src-ui/store.js b/src-ui/store.js index e648f946..d1712f1c 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -119,7 +119,7 @@ export const { atomInstance: Atom_MainFunctionsStateMemory, useHook: useStore_Ma export const { atomInstance: Atom_OpenedQuickSetting, useHook: useStore_OpenedQuickSetting } = createAtomWithHook("", "OpenedQuickSetting"); export const { atomInstance: Atom_LatestSoftwareVersionInfo, useHook: useStore_LatestSoftwareVersionInfo } = createAtomWithHook({ is_update_available: false, - new_version: "", + 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"); From 819783ea0909addaaa9e7d955564ac3497262654 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:55:28 +0900 Subject: [PATCH 43/47] [Update] Plugins: Add localization system. --- src-ui/app/_app_controllers/PluginsController.jsx | 2 ++ src-ui/logics/configs/plugins/usePlugins.js | 4 +++- src-ui/plugins/dev_plugin_subtitles/index.jsx | 7 +++++-- src-ui/plugins/dev_plugin_subtitles/locales/en.yml | 2 ++ .../dev_plugin_subtitles/locales/initI18n.js | 14 ++++++++++++++ src-ui/plugins/dev_plugin_subtitles/locales/ja.yml | 2 ++ .../locales/usePluginTranslation.jsx | 7 +++++++ .../plugins/dev_plugin_subtitles/plugin_configs.js | 2 ++ .../plugins/dev_plugin_subtitles/plugin_info.json | 6 +++--- .../SubtitleSystemContainer.jsx | 6 +++++- 10 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 src-ui/plugins/dev_plugin_subtitles/locales/en.yml create mode 100644 src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js create mode 100644 src-ui/plugins/dev_plugin_subtitles/locales/ja.yml create mode 100644 src-ui/plugins/dev_plugin_subtitles/locales/usePluginTranslation.jsx diff --git a/src-ui/app/_app_controllers/PluginsController.jsx b/src-ui/app/_app_controllers/PluginsController.jsx index 0abeb5cb..9980f080 100644 --- a/src-ui/app/_app_controllers/PluginsController.jsx +++ b/src-ui/app/_app_controllers/PluginsController.jsx @@ -1,9 +1,11 @@ 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"; diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 309c012c..7e4e3be9 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -50,6 +50,7 @@ export const usePlugins = () => { const { asyncTauriFetchGithub } = useFetch(); + const { i18n } = useTranslation(); const generatePluginContext = (downloaded_plugin_info) => { const plugin_context = { @@ -69,7 +70,8 @@ export const usePlugins = () => { }, createAtomWithHook: (...args) => createAtomWithHook(...args), - logics: { ...logics_common, ...logics_configs, ...logics_main } + logics: { ...logics_common, ...logics_configs, ...logics_main }, + i18n: i18n, }; return plugin_context; } diff --git a/src-ui/plugins/dev_plugin_subtitles/index.jsx b/src-ui/plugins/dev_plugin_subtitles/index.jsx index 1b8942d4..fc496275 100644 --- a/src-ui/plugins/dev_plugin_subtitles/index.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/index.jsx @@ -1,10 +1,13 @@ 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) => { - initStore(plugin_context.createAtomWithHook); - const { logics } = plugin_context; + const { createAtomWithHook, i18n, logics } = plugin_context; + + initStore(createAtomWithHook); + initI18n(i18n); const EntryComponents = () => { return ( 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..8154ded5 --- /dev/null +++ b/src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js @@ -0,0 +1,14 @@ +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; + // parse once + const en_res = en; + const ja_res = ja; + + // addResourceBundle will merge into i18n’s store + i18n.addResourceBundle("en", ns, en_res, /* deep = */ true, /* overwrite = */ true); + i18n.addResourceBundle("ja", ns, ja_res, /* 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 index 0944d0b6..36f116d1 100644 --- a/src-ui/plugins/dev_plugin_subtitles/plugin_configs.js +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_configs.js @@ -1,5 +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 index 3bb37720..c5e3023a 100644 --- a/src-ui/plugins/dev_plugin_subtitles/plugin_info.json +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json @@ -3,7 +3,7 @@ "plugin_id": "vrct_plugin_subtitles", "asset_name": "vrct_plugin_subtitles.zip", "location": "main_section", - "plugin_version": "0.0.0", - "min_supported_vrct_version": "3.0.4", - "max_supported_vrct_version": "3.0.6" + "plugin_version": "0.0.4", + "min_supported_vrct_version": "3.0.5", + "max_supported_vrct_version": "3.0.5" } \ No newline at end of file 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 index 144aefb0..8a1874f9 100644 --- a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx @@ -4,8 +4,10 @@ import { ModeSelectorContainer } from "./mode_selector_container/ModeSelectorCon 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); @@ -31,9 +33,11 @@ export const SubtitleSystemContainer = () => { // // カウントダウンタイマー専用の ref // const countdownIntervalRef = useRef(null); + console.log(t("main_page.title")); + return (
-

字幕プレイヤー

+

{t("main_page.title")}

From c665f1d3559c62d2f101eaabc3fa59e777bbc33e Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 26 Apr 2025 04:13:18 +0900 Subject: [PATCH 44/47] [Update/bugfix] Plugins: Add localization system to plugins list(plugin_info.json). Fix the issue that the i18n didn't work in development environment. --- package-lock.json | 35 ++++++++- package.json | 2 +- .../setting_box/plugins/Plugins.jsx | 72 +++++++++++-------- .../dev_plugin_subtitles/locales/initI18n.js | 7 +- .../dev_plugin_subtitles/plugin_info.json | 15 +++- .../SubtitleSystemContainer.jsx | 2 - vite.config.js | 8 ++- 7 files changed, 98 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85dac096..308c262d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "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", @@ -30,10 +29,11 @@ "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", - "source-map": "^0.7.4", + "source-map": "0.7.4", "vite": "6.2.5", "vite-plugin-svgr": "4.3.0" } @@ -1221,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", @@ -5353,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", diff --git a/package.json b/package.json index de100d5d..f9948b13 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "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", @@ -45,6 +44,7 @@ "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", diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx index b42ea31b..d0715698 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx @@ -26,7 +26,7 @@ export const Plugins = () => { }; const PluginDownloadContainer = () => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { downloadAndExtractPlugin, currentPluginsData, @@ -74,35 +74,49 @@ const PluginDownloadContainer = () => {
{is_failed_to_fetch &&

Failed to fetch plugins data

} {is_fetching &&

Fetching plugins data...

} - {sorted_plugins_data.map((plugin) => ( -
-

- {plugin.is_downloaded - ? plugin.downloaded_plugin_info?.title - : plugin.latest_plugin_info?.title} -

-

{plugin.plugin_id}

- {plugin.is_error ? ( -

Error: {plugin.error_message}

- ) : ( -
-
-

- {plugin.is_downloaded - ? `現在のバージョン: ${plugin.downloaded_plugin_info?.plugin_version}` - : null} -

+ {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} +

+

{plugin.plugin_id}

+

+ {target_locale.desc} +

+ {plugin.is_error ? ( +

Error: {plugin.error_message}

+ ) : ( +
+
+

+ {plugin.is_downloaded + ? `現在のバージョン: ${plugin.downloaded_plugin_info?.plugin_version}` + : null} +

+
+
- -
- )} -
- ))} + )} +
+ ); + })}
); }; \ 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 index 8154ded5..6e71957e 100644 --- a/src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js +++ b/src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js @@ -4,11 +4,8 @@ import plugin_info from "../plugin_info.json"; export const initI18n = (i18n) => { const ns = plugin_info.plugin_id; - // parse once - const en_res = en; - const ja_res = ja; // addResourceBundle will merge into i18n’s store - i18n.addResourceBundle("en", ns, en_res, /* deep = */ true, /* overwrite = */ true); - i18n.addResourceBundle("ja", ns, ja_res, /* deep = */ true, /* overwrite = */ true); + 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/plugin_info.json b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json index c5e3023a..ec673298 100644 --- a/src-ui/plugins/dev_plugin_subtitles/plugin_info.json +++ b/src-ui/plugins/dev_plugin_subtitles/plugin_info.json @@ -1,9 +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.4", + "plugin_version": "0.0.5", "min_supported_vrct_version": "3.0.5", - "max_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/subtitle_system_container/SubtitleSystemContainer.jsx b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx index 8a1874f9..64349850 100644 --- a/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx +++ b/src-ui/plugins/dev_plugin_subtitles/subtitle_system_container/SubtitleSystemContainer.jsx @@ -33,8 +33,6 @@ export const SubtitleSystemContainer = () => { // // カウントダウンタイマー専用の ref // const countdownIntervalRef = useRef(null); - console.log(t("main_page.title")); - return (

{t("main_page.title")}

diff --git a/vite.config.js b/vite.config.js index b46e13f1..78a27f37 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,6 +1,7 @@ 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"; @@ -11,8 +12,11 @@ export default defineConfig(async () => { const plugin_aliases = await getPluginAliases(); return { - plugins: [react(), svgr()], - assetsInclude: ["**/*.yml"], + plugins: [ + yaml({ include: ["**/*.yml", "**/*.yaml"] }), + react(), + svgr(), + ], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // From 56936d0285d93018eb68fc2581dac4291619244f Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 26 Apr 2025 19:41:28 +0900 Subject: [PATCH 45/47] [Update] Plugins -> AdvancedSettings. Move to AdvancedSettings because the plugin just only one is available and it's for specific user, actually. so put it to AdvancedSettings for now. --- locales/en.yml | 3 +- locales/ja.yml | 3 +- .../setting_box/SettingBox.jsx | 6 ++-- .../advanced_settings/AdvancedSettings.jsx | 29 ++++++++++++++----- .../AdvancedSettings.module.scss | 21 ++------------ .../plugins/Plugins.jsx | 2 +- .../plugins/Plugins.module.scss | 0 .../setting_section/setting_box/index.js | 2 +- .../sidebar_section/SidebarSection.jsx | 2 +- 9 files changed, 34 insertions(+), 34 deletions(-) rename src-ui/app/config_page/setting_section/setting_box/{ => advanced_settings}/plugins/Plugins.jsx (95%) rename src-ui/app/config_page/setting_section/setting_box/{ => advanced_settings}/plugins/Plugins.module.scss (100%) diff --git a/locales/en.yml b/locales/en.yml index a72200cb..cdefbfe5 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -244,6 +244,7 @@ config_page: label: "Open Config File" switch_compute_device: label: "Switch VRCT To CPU/GPU Version" + section_label_plugins: Plugins plugin_notifications: downloading: Downloading the plugin. @@ -254,7 +255,7 @@ plugin_notifications: 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. + 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..e4c40eb5 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -243,4 +243,5 @@ 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: プラグイン \ 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 b149da0d..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,7 +9,7 @@ import { AdvancedSettings, Vr, Hotkeys, - Plugins, + // Plugins, Supporters, AboutVrct, } from "@setting_box"; @@ -33,8 +33,8 @@ export const SettingBox = () => { return ; case "advanced_settings": return ; - case "plugins": - return ; + // case "plugins": + // return ; case "supporters": return ; case "about_vrct": 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/plugins/Plugins.jsx b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.jsx similarity index 95% rename from src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx rename to src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.jsx index d0715698..850e83dd 100644 --- a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.jsx @@ -2,7 +2,7 @@ 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 { PluginsControlComponent } from "../../_components/plugins_control_component/PluginsControlComponent"; import { useNotificationStatus } from "@logics_common"; export const Plugins = () => { diff --git a/src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.module.scss similarity index 100% rename from src-ui/app/config_page/setting_section/setting_box/plugins/Plugins.module.scss rename to src-ui/app/config_page/setting_section/setting_box/advanced_settings/plugins/Plugins.module.scss 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 abb876ae..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,6 +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 { 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/sidebar_section/SidebarSection.jsx b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx index e1691ce9..f845a733 100644 --- a/src-ui/app/config_page/sidebar_section/SidebarSection.jsx +++ b/src-ui/app/config_page/sidebar_section/SidebarSection.jsx @@ -12,7 +12,7 @@ export const SidebarSection = () => { - + {/* */}
From e3b75f2cfb6af9b0f0e50bed90ecea7e9a839d68 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 27 Apr 2025 02:15:31 +0900 Subject: [PATCH 46/47] [Update] Plugins: Adjust designs. Add localizations. --- locales/en.yml | 14 ++++- locales/ja.yml | 28 ++++++++- .../PluginsControlComponent.jsx | 63 +++++++++++++------ .../switch_box/SwitchBox.module.scss | 1 - .../advanced_settings/plugins/Plugins.jsx | 41 ++++++------ .../plugins/Plugins.module.scss | 39 ++++++++---- 6 files changed, 131 insertions(+), 55 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index cdefbfe5..f72907ac 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -244,7 +244,19 @@ config_page: label: "Open Config File" switch_compute_device: label: "Switch VRCT To CPU/GPU Version" - section_label_plugins: Plugins + 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. diff --git a/locales/ja.yml b/locales/ja.yml index e4c40eb5..3d109b8a 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -244,4 +244,30 @@ config_page: label: "設定ファイルを開く" switch_compute_device: label: "VRCT CPU/GPUバージョンの切り替え" - section_label_plugins: プラグイン \ No newline at end of file + 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/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 index 9c33c90d..c7c6ace0 100644 --- 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 @@ -2,6 +2,7 @@ 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, @@ -9,6 +10,8 @@ export const PluginsControlComponent = ({ toggleFunction, downloadStartFunction, }) => { + const { t } = useTranslation(); + const option = { id: plugin_status.plugin_id, is_pending: plugin_status.is_pending, @@ -19,6 +22,16 @@ export const PluginsControlComponent = ({ 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 { @@ -34,6 +49,8 @@ export const PluginsControlComponent = ({ option={option} plugin_status={plugin_status} downloadStartFunction={downloadStartFunction} + downloaded_version_label={downloaded_version_label} + latest_version_label={latest_version_label} /> ); } @@ -45,49 +62,51 @@ const DownloadedPluginControl = ({ plugin_status, toggleFunction, downloadStartFunction, + downloaded_version_label, + latest_version_label, }) => { + const { t } = useTranslation(); + const togglePlugin = () => { toggleFunction(plugin_status.plugin_id); }; - const latest_version = plugin_status.latest_plugin_info?.plugin_version; - - if (!plugin_status.downloaded_plugin_info.is_plugin_supported) { if (plugin_status.is_latest_version_available) { return (
-

最新のバージョン: {latest_version}

-

最新版にアップデート後 利用可能

+

{downloaded_version_label}

+

{latest_version_label}

+

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

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

現在利用不可 使用中VRCTバージョンとの互換性なし

+

{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}

-

最新版を使用中

+

{latest_version_label}

+

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

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

最新のバージョン: {latest_version}

-

最新版を利用可能

+

{latest_version_label}

+

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

<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
@@ -95,7 +114,7 @@ const DownloadedPluginControl = ({ } else { return (
-

最新版は現在利用不可

+

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

); @@ -103,28 +122,34 @@ const DownloadedPluginControl = ({ }; -const NotDownloadedPluginControl = ({ option, plugin_status, downloadStartFunction }) => { - const latest_version = plugin_status.latest_plugin_info?.plugin_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}

+

{latest_version_label}

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

最新のバージョン: {latest_version}

-

VRCT最新版で利用可能

+

{latest_version_label}

+

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

); } else { return (
-

最新バージョン: {latest_version}

-

現在利用不可

+

{latest_version_label}

+

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

); } 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 29c26df5..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; 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 index 850e83dd..272fff80 100644 --- 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 @@ -61,14 +61,18 @@ const PluginDownloadContainer = () => { const variable_state = currentSavedPluginsStatus.state; + const filtered_plugins_data = currentPluginsData.data.filter(plugin => !plugin.is_outdated) + // plugin_id で ABC 順にソート - const sorted_plugins_data = [...currentPluginsData.data].sort((a, b) => + 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"; + console.log(sorted_plugins_data); + return (
@@ -88,32 +92,27 @@ const PluginDownloadContainer = () => { return (
-

- {target_locale.title} -

-

{plugin.plugin_id}

-

- {target_locale.desc} -

- {plugin.is_error ? ( -

Error: {plugin.error_message}

- ) : ( -
-
-

- {plugin.is_downloaded - ? `現在のバージョン: ${plugin.downloaded_plugin_info?.plugin_version}` - : null} -

-
+
+

+ {target_locale.title} +

+

+ {target_locale.desc} +

+ {/*

{plugin.plugin_id}

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

Error: {plugin.error_message}

+ ) : ( -
- )} + )} +
); })} 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 index 3320d590..c65a82ec 100644 --- 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 @@ -13,30 +13,45 @@ .plugin_wrapper { width: 100%; display: flex; - flex-direction: column; + justify-content: space-between; + align-items: center; padding: 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; - width: 100%; - justify-content: space-between; + justify-content: end; align-items: center; } .title { font-size: 1.6rem; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } - -.plugin_id { - font-size: 1rem; +.desc { + font-size: 1.4rem; + width: 100%; + overflow: hidden; + color: var(--dark_500_color); } - -.download_button { - background-color: var(--dark_750_color); - padding: 0.4rem 0.6rem; - font-size: 1.2rem; -} \ No newline at end of file +// .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 From 7cdd3f8603ab63463d5a57bb8f45d1d6e503f084 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 27 Apr 2025 02:27:25 +0900 Subject: [PATCH 47/47] [bugfix/Chore] Plugins: Fix the bug that localization and the description can't see form latest information. Add gap between labels and controller component. --- .../setting_box/advanced_settings/plugins/Plugins.jsx | 2 -- .../advanced_settings/plugins/Plugins.module.scss | 1 + src-ui/logics/configs/plugins/usePlugins.js | 9 ++------- 3 files changed, 3 insertions(+), 9 deletions(-) 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 index 272fff80..b19d48d7 100644 --- 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 @@ -71,8 +71,6 @@ const PluginDownloadContainer = () => { // Duplicate const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error"; const is_fetching = currentFetchedPluginsInfo.state === "pending"; - console.log(sorted_plugins_data); - return (
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 index c65a82ec..af4bae9a 100644 --- 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 @@ -16,6 +16,7 @@ justify-content: space-between; align-items: center; padding: 2rem; + gap: 2rem; &:not(:last-child) { border-bottom: 0.1rem solid var(--dark_750_color); } diff --git a/src-ui/logics/configs/plugins/usePlugins.js b/src-ui/logics/configs/plugins/usePlugins.js index 7e4e3be9..8bd533e0 100644 --- a/src-ui/logics/configs/plugins/usePlugins.js +++ b/src-ui/logics/configs/plugins/usePlugins.js @@ -280,15 +280,10 @@ export const usePlugins = () => { const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(plugin_info.min_supported_vrct_version, plugin_info.max_supported_vrct_version); return { - title: plugin_info.title, - plugin_id: plugin_info.plugin_id, - plugin_version: plugin_info.plugin_version, - min_supported_vrct_version: plugin_info.min_supported_vrct_version, - max_supported_vrct_version: plugin_info.max_supported_vrct_version, + ...plugin_info, is_plugin_supported: is_plugin_supported, is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct, - asset_name: plugin_info.asset_name, - url: plugin_info_asset_url + url: plugin_info_asset_url, }; }