[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)
This commit is contained in:
Sakamoto Shiina
2025-03-13 12:17:36 +09:00
parent 77795192a0
commit b0f5751e11
17 changed files with 350 additions and 237 deletions

View File

@@ -41,7 +41,6 @@ export const App = () => {
<FontFamilyController />
<TransparencyController />
<WindowGeometryController />
<PluginsController />
{(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false)
? <SplashComponent />
@@ -57,6 +56,7 @@ const Contents = () => {
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
return (
<>
<PluginsController />
<SubtitlesController />
<WindowTitleBar />

View File

@@ -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;

View File

@@ -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 (
<>
<CircularProgress
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
value={circular_progress}
size="3rem"
sx={{ color: "var(--primary_300_color)" }}
/>
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
</>
);
case option.is_pending:
return <CircularProgress size="3rem" sx={{ color: "var(--dark_600_color)" }}/>;
case !option.is_downloaded:
return (
<button
className={styles.download_button}
onClick={() => props.downloadStartFunction(option.id)}
>
<p className={styles.download_button_label}>{t("config_page.model_download_button_label")}</p>
</button>
);
default:
return null;
}
};
return <div className={styles.download_container}>{renderContent()}</div>;
};

View File

@@ -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;
}

View File

@@ -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 (
<>
<CircularProgress
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
value={circular_progress}
size="3rem"
sx={{ color: "var(--primary_300_color)" }}
/>
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
</>
);
case option.is_pending:
return <CircularProgress size="3rem" sx={{ color: "var(--dark_600_color)" }}/>;
case !option.is_downloaded:
return (
<button
className={styles.download_button}
onClick={() => props.downloadStartFunction(option.id)}
>
<p className={styles.download_button_label}>{t("config_page.model_download_button_label")}</p>
</button>
);
default:
return null;
}
};
return <div className={styles.download_container}>{renderContent()}</div>;
};
};

View File

@@ -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;
}

View File

@@ -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 (
<div>
{/* <SwitchBox
variable={currentEnableAutoMicSelect}
toggleFunction={toggleEnableAutoMicSelect}
/> */}
{plugin_info.is_plugin_supported ?
<_DownloadButton
option={option}
downloadStartFunction={props.downloadStartFunction}
/>
:
<div>
Unavailable
</div>
}
</div>
);
};

View File

@@ -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";
export { DownloadModels } from "./download_models/DownloadModels";
export { DownloadPlugins } from "./download_plugins/DownloadPlugins";

View File

@@ -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 (
<div>
<div className={styles.plugins_list_container}>
{plugin_list.map((plugin) => (
<div key={plugin.plugin_id}>
<h3>{plugin.title}</h3>
<h4>{plugin.plugin_id}</h4>
<div key={plugin.plugin_id} className={styles.plugin_wrapper}>
<p className={styles.title}>{plugin.title}</p>
<p className={styles.plugin_id}>{plugin.plugin_id}</p>
{plugin.error ? (
<p style={{ color: "red" }}>Error: {plugin.error}</p>
) : (
<>
<p>Version: {plugin.plugin_version}</p>
<p>
Compatible: {plugin.min_compatible_version} ~ {plugin.max_compatible_version}
</p>
<button onClick={() => handleDownload(plugin)}>
Download and Load Plugin
</button>
{download_progress[plugin.plugin_id] !== undefined && (
<div>
Download Progress: {download_progress[plugin.plugin_id].toFixed(0)}%
</div>
)}
</>
<div className={styles.plugin_info_wrapper}>
<div className={styles.plugin_info}>
<p>Version: {plugin.plugin_version}</p>
<p>
Compatible: {plugin.min_supported_vrct_version} ~ {plugin.max_supported_vrct_version}
</p>
</div>
<DownloadPlugins
plugin_info={plugin}
downloadStartFunction={downloadStartFunction}
/>
</div>
)}
</div>
))}
</div>
);
};
// 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 バージョンと互換性がありません。");
// }
};

View File

@@ -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;
}

View File

@@ -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 (