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");