Merge branch 'plugins_system' into develop
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,4 +43,5 @@ dist-ssr
|
|||||||
.venv
|
.venv
|
||||||
|
|
||||||
# Customize
|
# Customize
|
||||||
/build
|
/build
|
||||||
|
error.txt
|
||||||
@@ -243,4 +243,31 @@ config_page:
|
|||||||
open_config_filepath:
|
open_config_filepath:
|
||||||
label: "Open Config File"
|
label: "Open Config File"
|
||||||
switch_compute_device:
|
switch_compute_device:
|
||||||
label: "Switch VRCT To CPU/GPU Version"
|
label: "Switch VRCT To CPU/GPU Version"
|
||||||
|
section_label_plugins: Plugins # Exception, It'll be moved later.
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "Downloaded version: {{downloaded_version}}"
|
||||||
|
latest_version: "Latest version: {{latest_version}}"
|
||||||
|
available_after_updating: "Available after updating to the latest version"
|
||||||
|
unavailable_downloaded: "Currently unavailable due to incompatibility with the VRCT version in use"
|
||||||
|
no_latest_info: "Unable to retrieve the latest information"
|
||||||
|
using_latest_version: "Using the latest version"
|
||||||
|
available_latest_version: "Latest version available"
|
||||||
|
unavailable_latest_version: "Latest version currently unavailable"
|
||||||
|
available_in_latest_vrct_version: "Available in the latest VRCT version"
|
||||||
|
unavailable_not_downloaded: "Currently unavailable"
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: Downloading the plugin.
|
||||||
|
downloaded_success: Downloaded successfully.
|
||||||
|
downloaded_error: Download failed.
|
||||||
|
|
||||||
|
updating: Updating the plugin.
|
||||||
|
updated_success: Updated successfully.
|
||||||
|
updated_error: Update failed.
|
||||||
|
|
||||||
|
disabled_out_of_support: The plugin has been disabled. It's not supported on this VRCT version.
|
||||||
|
|
||||||
|
is_enabled: The plugin has enabled.
|
||||||
|
is_disabled: The plugin has disabled.
|
||||||
@@ -243,4 +243,31 @@ config_page:
|
|||||||
open_config_filepath:
|
open_config_filepath:
|
||||||
label: "設定ファイルを開く"
|
label: "設定ファイルを開く"
|
||||||
switch_compute_device:
|
switch_compute_device:
|
||||||
label: "VRCT CPU/GPUバージョンの切り替え"
|
label: "VRCT CPU/GPUバージョンの切り替え"
|
||||||
|
section_label_plugins: プラグイン # Exception, It'll be moved later.
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
downloaded_version: "ダウンロード済バージョン: {{downloaded_version}}"
|
||||||
|
latest_version: "最新バージョン: {{latest_version}}"
|
||||||
|
available_after_updating: 最新版にアップデート後 利用可能
|
||||||
|
unavailable_downloaded: 現在利用不可 使用中VRCTバージョンとの互換性なし
|
||||||
|
no_latest_info: 最新情報が取得できません
|
||||||
|
using_latest_version: 最新版を使用中
|
||||||
|
available_latest_version: 最新版を利用可能
|
||||||
|
unavailable_latest_version: 最新版は現在利用不可
|
||||||
|
available_in_latest_vrct_version: VRCT最新版で利用可能
|
||||||
|
unavailable_not_downloaded: 現在利用不可
|
||||||
|
|
||||||
|
plugin_notifications:
|
||||||
|
downloading: プラグインをダウンロード中。
|
||||||
|
downloaded_success: プラグインのダウンロードが完了しました。
|
||||||
|
downloaded_error: プラグインのダウンロードに失敗しました。
|
||||||
|
|
||||||
|
updating: プラグインをアップデート中。
|
||||||
|
updated_success: プラグインのアップデートが完了しました。
|
||||||
|
updated_error: プラグインのアップデートに失敗しました。
|
||||||
|
|
||||||
|
disabled_out_of_support: 現在のバージョンとの互換性がありません。プラグインを無効にしました。
|
||||||
|
|
||||||
|
is_enabled: プラグインを有効にしました。
|
||||||
|
is_disabled: プラグインを無効にしました。
|
||||||
78
map-stack.js
Normal file
78
map-stack.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
621
package-lock.json
generated
621
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -23,6 +23,7 @@
|
|||||||
"release-all": "npm run release && npm run release-cuda"
|
"release-all": "npm run release && npm run release-cuda"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/standalone": "7.26.9",
|
||||||
"@emotion/react": "11.14.0",
|
"@emotion/react": "11.14.0",
|
||||||
"@emotion/styled": "11.14.0",
|
"@emotion/styled": "11.14.0",
|
||||||
"@mui/material": "6.2.0",
|
"@mui/material": "6.2.0",
|
||||||
@@ -34,17 +35,21 @@
|
|||||||
"i18next": "24.1.0",
|
"i18next": "24.1.0",
|
||||||
"jotai": "2.10.3",
|
"jotai": "2.10.3",
|
||||||
"js-base64": "3.7.7",
|
"js-base64": "3.7.7",
|
||||||
"js-yaml": "4.1.0",
|
"jszip": "3.10.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-error-boundary": "5.0.0",
|
||||||
"react-i18next": "15.2.0",
|
"react-i18next": "15.2.0",
|
||||||
"react-resizable-layout": "0.7.2"
|
"react-resizable-layout": "0.7.2",
|
||||||
|
"semver": "7.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-yaml": "^4.1.2",
|
||||||
"@tauri-apps/cli": "1.6.3",
|
"@tauri-apps/cli": "1.6.3",
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"sass": "1.79.4",
|
"sass": "1.79.4",
|
||||||
"vite": "6.0.3",
|
"source-map": "0.7.4",
|
||||||
|
"vite": "6.2.5",
|
||||||
"vite-plugin-svgr": "4.3.0"
|
"vite-plugin-svgr": "4.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -555,6 +555,18 @@ class Config:
|
|||||||
self._HOTKEYS[key] = value
|
self._HOTKEYS[key] = value
|
||||||
self.saveConfig(inspect.currentframe().f_code.co_name, self.HOTKEYS, immediate_save=True)
|
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
|
@property
|
||||||
@json_serializable('MIC_AVG_LOGPROB')
|
@json_serializable('MIC_AVG_LOGPROB')
|
||||||
def MIC_AVG_LOGPROB(self):
|
def MIC_AVG_LOGPROB(self):
|
||||||
@@ -1068,6 +1080,7 @@ class Config:
|
|||||||
"toggle_transcription_send": None,
|
"toggle_transcription_send": None,
|
||||||
"toggle_transcription_receive": None,
|
"toggle_transcription_receive": None,
|
||||||
}
|
}
|
||||||
|
self._PLUGINS_STATUS = []
|
||||||
self._MIC_AVG_LOGPROB = -0.8
|
self._MIC_AVG_LOGPROB = -0.8
|
||||||
self._MIC_NO_SPEECH_PROB = 0.6
|
self._MIC_NO_SPEECH_PROB = 0.6
|
||||||
self._AUTO_SPEAKER_SELECT = True
|
self._AUTO_SPEAKER_SELECT = True
|
||||||
|
|||||||
@@ -447,11 +447,11 @@ class Controller:
|
|||||||
return {"status":200, "result":config.VERSION}
|
return {"status":200, "result":config.VERSION}
|
||||||
|
|
||||||
def checkSoftwareUpdated(self) -> dict:
|
def checkSoftwareUpdated(self) -> dict:
|
||||||
update_flag = model.checkSoftwareUpdated()
|
software_update_info = model.checkSoftwareUpdated()
|
||||||
self.run(
|
self.run(
|
||||||
200,
|
200,
|
||||||
self.run_mapping["update_software_flag"],
|
self.run_mapping["software_update_info"],
|
||||||
update_flag,
|
software_update_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1061,6 +1061,15 @@ class Controller:
|
|||||||
config.HOTKEYS = data
|
config.HOTKEYS = data
|
||||||
return {"status":200, "result":config.HOTKEYS}
|
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
|
@staticmethod
|
||||||
def getSpeakerAvgLogprob(*args, **kwargs) -> dict:
|
def getSpeakerAvgLogprob(*args, **kwargs) -> dict:
|
||||||
return {"status":200, "result":config.SPEAKER_AVG_LOGPROB}
|
return {"status":200, "result":config.SPEAKER_AVG_LOGPROB}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ run_mapping = {
|
|||||||
"mic_device_list":"/run/mic_device_list",
|
"mic_device_list":"/run/mic_device_list",
|
||||||
"speaker_device_list":"/run/speaker_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_progress":"/run/initialization_progress",
|
||||||
"initialization_complete":"/run/initialization_complete",
|
"initialization_complete":"/run/initialization_complete",
|
||||||
@@ -190,6 +190,9 @@ mapping = {
|
|||||||
"/get/data/hotkeys": {"status": True, "variable":controller.getHotkeys},
|
"/get/data/hotkeys": {"status": True, "variable":controller.getHotkeys},
|
||||||
"/set/data/hotkeys": {"status": True, "variable":controller.setHotkeys},
|
"/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},
|
"/get/data/mic_avg_logprob": {"status": True, "variable":controller.getMicAvgLogprob},
|
||||||
"/set/data/mic_avg_logprob": {"status": True, "variable":controller.setMicAvgLogprob},
|
"/set/data/mic_avg_logprob": {"status": True, "variable":controller.setMicAvgLogprob},
|
||||||
|
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ class Model:
|
|||||||
def checkSoftwareUpdated():
|
def checkSoftwareUpdated():
|
||||||
# check update
|
# check update
|
||||||
update_flag = False
|
update_flag = False
|
||||||
|
version = ""
|
||||||
try:
|
try:
|
||||||
response = requests_get(config.GITHUB_URL)
|
response = requests_get(config.GITHUB_URL)
|
||||||
json_data = response.json()
|
json_data = response.json()
|
||||||
@@ -331,7 +332,10 @@ class Model:
|
|||||||
update_flag = True
|
update_flag = True
|
||||||
except Exception:
|
except Exception:
|
||||||
errorLogging()
|
errorLogging()
|
||||||
return update_flag
|
return {
|
||||||
|
"is_update_available": update_flag,
|
||||||
|
"new_version": version,
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def updateSoftware():
|
def updateSoftware():
|
||||||
|
|||||||
507
src-tauri/Cargo.lock
generated
507
src-tauri/Cargo.lock
generated
@@ -6,7 +6,9 @@ version = 3
|
|||||||
name = "VRCT"
|
name = "VRCT"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"font-kit",
|
"font-kit",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
@@ -218,6 +220,9 @@ name = "bytes"
|
|||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
@@ -729,7 +734,7 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.8.19",
|
"toml 0.8.19",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -971,6 +976,12 @@ dependencies = [
|
|||||||
"syn 2.0.94",
|
"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]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -984,8 +995,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"slab",
|
"slab",
|
||||||
@@ -1297,6 +1311,25 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1361,12 +1394,86 @@ dependencies = [
|
|||||||
"itoa 1.0.14",
|
"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]]
|
[[package]]
|
||||||
name = "http-range"
|
name = "http-range"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
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]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.61"
|
version = "0.1.61"
|
||||||
@@ -1613,6 +1720,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -1849,6 +1962,12 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -1859,6 +1978,34 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"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]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -1998,6 +2145,50 @@ dependencies = [
|
|||||||
"windows-sys 0.42.0",
|
"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]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2516,6 +2707,67 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@@ -2544,6 +2796,37 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.19"
|
version = "1.0.19"
|
||||||
@@ -2565,6 +2848,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -2577,6 +2869,39 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
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]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -2659,6 +2984,18 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.12.0"
|
version = "3.12.0"
|
||||||
@@ -2784,6 +3121,16 @@ version = "1.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
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]]
|
[[package]]
|
||||||
name = "soup2"
|
name = "soup2"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -2812,6 +3159,12 @@ dependencies = [
|
|||||||
"system-deps 5.0.0",
|
"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]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2881,6 +3234,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@@ -2892,6 +3251,27 @@ dependencies = [
|
|||||||
"syn 2.0.94",
|
"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]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
@@ -3000,6 +3380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22"
|
checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bytes",
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"dunce",
|
"dunce",
|
||||||
@@ -3014,6 +3395,7 @@ dependencies = [
|
|||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"http",
|
"http",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"indexmap 1.9.3",
|
||||||
"log",
|
"log",
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -3024,6 +3406,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"regex",
|
"regex",
|
||||||
|
"reqwest",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3296,7 +3679,44 @@ checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
"pin-project-lite",
|
"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]]
|
[[package]]
|
||||||
@@ -3367,6 +3787,12 @@ dependencies = [
|
|||||||
"winnow 0.6.22",
|
"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]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.41"
|
||||||
@@ -3428,6 +3854,12 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "try-lock"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -3446,6 +3878,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
@@ -3491,6 +3929,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.0.11"
|
version = "0.0.11"
|
||||||
@@ -3539,6 +3983,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.9.0+wasi-snapshot-preview1"
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
@@ -3576,6 +4029,19 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"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]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.99"
|
version = "0.2.99"
|
||||||
@@ -3605,6 +4071,29 @@ version = "0.2.99"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
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]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -3652,6 +4141,12 @@ dependencies = [
|
|||||||
"system-deps 6.2.2",
|
"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]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.19.1"
|
version = "0.19.1"
|
||||||
@@ -4058,6 +4553,16 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ edition = "2021"
|
|||||||
tauri-build = { version = "1", features = [] }
|
tauri-build = { version = "1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1", features = [ "window-hide", "window-set-focus", "global-shortcut-all", "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] }
|
tauri = { version = "1", features = [ "fs-remove-dir", "fs-read-file", "fs-create-dir", "fs-write-file", "fs-exists", "http-request", "fs-read-dir", "window-hide", "window-set-focus", "global-shortcut-all", "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
font-kit = "0.14.2"
|
font-kit = "0.14.2"
|
||||||
window-shadows = { git = "https://github.com/tauri-apps/window-shadows.git" }
|
window-shadows = { git = "https://github.com/tauri-apps/window-shadows.git" }
|
||||||
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
1
src-tauri/plugins/index.js
Normal file
1
src-tauri/plugins/index.js
Normal file
@@ -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.
|
||||||
@@ -22,7 +22,7 @@ fn main() {
|
|||||||
event.window().set_size(tauri::Size::Physical(*new_inner_size)).unwrap();
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
@@ -45,4 +45,23 @@ async fn get_font_list() -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
font_families.into_iter().collect()
|
font_families.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn download_zip_asset(url: String) -> Result<String, String> {
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,24 @@
|
|||||||
"globalShortcut": {
|
"globalShortcut": {
|
||||||
"all": true
|
"all": true
|
||||||
},
|
},
|
||||||
|
"fs": {
|
||||||
|
"readDir": true,
|
||||||
|
"readFile": true,
|
||||||
|
"exists": true,
|
||||||
|
"writeFile": true,
|
||||||
|
"createDir": true,
|
||||||
|
"removeDir": true,
|
||||||
|
"scope": ["$RESOURCE/**", "**/src-tauri/target/debug/plugins/**"]
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"request": true,
|
||||||
|
"scope": [
|
||||||
|
"https://api.github.com/repos/**",
|
||||||
|
"https://github.com/**",
|
||||||
|
"https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json",
|
||||||
|
"https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/dev_vrct_plugins_list.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"all": false,
|
"all": false,
|
||||||
"open": true,
|
"open": true,
|
||||||
@@ -68,7 +86,8 @@
|
|||||||
"bin/VRCT-sidecar"
|
"bin/VRCT-sidecar"
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"bin/_internal": "_internal"
|
"bin/_internal": "_internal",
|
||||||
|
"plugins": "plugins"
|
||||||
},
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
"nsis": {
|
"nsis": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UiSizeController,
|
UiSizeController,
|
||||||
FontFamilyController,
|
FontFamilyController,
|
||||||
TransparencyController,
|
TransparencyController,
|
||||||
|
PluginsController,
|
||||||
} from "./_app_controllers/index.js";
|
} from "./_app_controllers/index.js";
|
||||||
|
|
||||||
import { WindowTitleBar } from "./window_title_bar/WindowTitleBar";
|
import { WindowTitleBar } from "./window_title_bar/WindowTitleBar";
|
||||||
@@ -20,6 +21,7 @@ import { ModalController } from "./modal_controller/ModalController";
|
|||||||
import { SnackbarController } from "./snackbar_controller/SnackbarController";
|
import { SnackbarController } from "./snackbar_controller/SnackbarController";
|
||||||
import styles from "./App.module.scss";
|
import styles from "./App.module.scss";
|
||||||
import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common";
|
import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common";
|
||||||
|
import { AppErrorBoundary } from "./error_boundary/AppErrorBoundary";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { currentIsVrctAvailable } = useIsVrctAvailable();
|
const { currentIsVrctAvailable } = useIsVrctAvailable();
|
||||||
@@ -29,22 +31,24 @@ export const App = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<KeyEventController />
|
<AppErrorBoundary >
|
||||||
<StartPythonController />
|
<KeyEventController />
|
||||||
<GlobalHotKeyController />
|
<StartPythonController />
|
||||||
<UiLanguageController />
|
<GlobalHotKeyController />
|
||||||
<ConfigPageCloseTriggerController />
|
<UiLanguageController />
|
||||||
<UiSizeController />
|
<ConfigPageCloseTriggerController />
|
||||||
<FontFamilyController />
|
<UiSizeController />
|
||||||
<TransparencyController />
|
<FontFamilyController />
|
||||||
<WindowGeometryController />
|
<TransparencyController />
|
||||||
|
<WindowGeometryController />
|
||||||
|
|
||||||
{(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false)
|
{(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false)
|
||||||
? <SplashComponent />
|
? <SplashComponent />
|
||||||
: <Contents key={i18n.language}/>
|
: <Contents key={i18n.language} />
|
||||||
}
|
}
|
||||||
|
|
||||||
<SnackbarController />
|
<SnackbarController />
|
||||||
|
</AppErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -53,6 +57,8 @@ const Contents = () => {
|
|||||||
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
|
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PluginsController />
|
||||||
|
|
||||||
<WindowTitleBar />
|
<WindowTitleBar />
|
||||||
{currentIsSoftwareUpdating.data === false
|
{currentIsSoftwareUpdating.data === false
|
||||||
?
|
?
|
||||||
|
|||||||
24
src-ui/app/_app_controllers/PluginsController.jsx
Normal file
24
src-ui/app/_app_controllers/PluginsController.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import * as reactI18next from "react-i18next";
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.React = React;
|
||||||
|
window.clsx = clsx;
|
||||||
|
window.reactI18next = reactI18next;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { LoadPluginsController } from "./plugins_controllers/LoadPluginsController";
|
||||||
|
import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController";
|
||||||
|
import { MergePluginsController } from "./plugins_controllers/MergePluginsController";
|
||||||
|
|
||||||
|
export const PluginsController = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MergePluginsController />
|
||||||
|
<LoadPluginsController />
|
||||||
|
<FetchLatestPluginsDataController />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,4 +5,5 @@ export { UiLanguageController } from "./UiLanguageController";
|
|||||||
export { ConfigPageCloseTriggerController } from "./ConfigPageCloseTriggerController";
|
export { ConfigPageCloseTriggerController } from "./ConfigPageCloseTriggerController";
|
||||||
export { UiSizeController } from "./UiSizeController";
|
export { UiSizeController } from "./UiSizeController";
|
||||||
export { FontFamilyController } from "./FontFamilyController";
|
export { FontFamilyController } from "./FontFamilyController";
|
||||||
export { TransparencyController } from "./TransparencyController";
|
export { TransparencyController } from "./TransparencyController";
|
||||||
|
export { PluginsController } from "./PluginsController";
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { usePlugins } from "@logics_configs";
|
||||||
|
|
||||||
|
export const FetchLatestPluginsDataController = () => {
|
||||||
|
const {
|
||||||
|
asyncFetchPluginsInfo,
|
||||||
|
isAnyPluginEnabled_Init,
|
||||||
|
} = usePlugins();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAnyPluginEnabled_Init()) {
|
||||||
|
asyncFetchPluginsInfo();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { usePlugins } from "@logics_configs";
|
||||||
|
import { store } from "@store";
|
||||||
|
|
||||||
|
export const LoadPluginsController = () => {
|
||||||
|
const {
|
||||||
|
asyncLoadAllPlugins,
|
||||||
|
} = usePlugins();
|
||||||
|
|
||||||
|
const asyncInitLoadPlugins = async () => {
|
||||||
|
try {
|
||||||
|
await asyncLoadAllPlugins();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!store.is_initialized_load_plugin) {
|
||||||
|
asyncInitLoadPlugins();
|
||||||
|
store.is_initialized_load_plugin = true;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { store } from "@store";
|
||||||
|
import { usePlugins } from "@logics_configs";
|
||||||
|
import { useSoftwareVersion } from "@logics_common";
|
||||||
|
import { useNotificationStatus } from "@logics_common";
|
||||||
|
|
||||||
|
export const MergePluginsController = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
currentLoadedPlugins,
|
||||||
|
updatePluginsData,
|
||||||
|
currentPluginsData,
|
||||||
|
currentFetchedPluginsInfo,
|
||||||
|
currentSavedPluginsStatus,
|
||||||
|
downloadAndExtractPlugin,
|
||||||
|
setTargetSavedPluginsStatus_Init,
|
||||||
|
} = usePlugins();
|
||||||
|
const { checkVrctVerCompatibility } = useSoftwareVersion();
|
||||||
|
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
|
||||||
|
|
||||||
|
// downloaded, fetched, saved の各情報をまとめてマージ
|
||||||
|
useEffect(() => {
|
||||||
|
const mergePluginData = () => {
|
||||||
|
updatePluginsData(prev => {
|
||||||
|
// downloaded, fetched, 保存済み状態のMapをそれぞれ作成(plugin_id をキー)
|
||||||
|
const downloaded_map = new Map(
|
||||||
|
currentLoadedPlugins.data.map(info => [info.plugin_id, info])
|
||||||
|
);
|
||||||
|
const fetched_map = new Map(
|
||||||
|
currentFetchedPluginsInfo.data.map(info => [info.plugin_id, info])
|
||||||
|
);
|
||||||
|
const saved_map = new Map(
|
||||||
|
currentSavedPluginsStatus.data.map(saved => [saved.plugin_id, saved])
|
||||||
|
);
|
||||||
|
const prev_map = new Map(
|
||||||
|
prev.data.map(item => [item.plugin_id, item])
|
||||||
|
);
|
||||||
|
|
||||||
|
// union_keys: Saved以外の情報に対して重複なくキーを取得する
|
||||||
|
const union_keys = new Set([
|
||||||
|
...downloaded_map.keys(),
|
||||||
|
...fetched_map.keys(),
|
||||||
|
...prev_map.keys(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const new_data = [];
|
||||||
|
for (const id of union_keys) {
|
||||||
|
const downloaded = downloaded_map.get(id);
|
||||||
|
const fetched = fetched_map.get(id);
|
||||||
|
const prev_plugin = prev_map.get(id);
|
||||||
|
let plugin = {};
|
||||||
|
|
||||||
|
if (downloaded) {
|
||||||
|
// ダウンロード済み情報に対してサポート確認
|
||||||
|
const { is_plugin_supported, is_plugin_supported_latest_vrct } =
|
||||||
|
checkVrctVerCompatibility(
|
||||||
|
downloaded.min_supported_vrct_version,
|
||||||
|
downloaded.max_supported_vrct_version
|
||||||
|
);
|
||||||
|
plugin = {
|
||||||
|
// prevの情報があれば引き継ぎつつ上書き
|
||||||
|
...(prev_plugin || {}),
|
||||||
|
plugin_id: downloaded.plugin_id,
|
||||||
|
component: downloaded.component,
|
||||||
|
is_downloaded: true,
|
||||||
|
downloaded_plugin_info: {
|
||||||
|
...downloaded,
|
||||||
|
is_plugin_supported,
|
||||||
|
is_plugin_supported_latest_vrct,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fetched) {
|
||||||
|
const is_latest_version_available =
|
||||||
|
(downloaded.plugin_version !== fetched.plugin_version && fetched.is_plugin_supported);
|
||||||
|
plugin = {
|
||||||
|
...plugin,
|
||||||
|
is_outdated: false,
|
||||||
|
latest_plugin_info: { ...fetched },
|
||||||
|
is_latest_version_available:
|
||||||
|
plugin.is_downloaded && is_latest_version_available,
|
||||||
|
is_latest_version_already:
|
||||||
|
downloaded.plugin_version === fetched.plugin_version,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// フェッチ情報がない場合の初期状態
|
||||||
|
plugin = {
|
||||||
|
...plugin,
|
||||||
|
is_latest_version_available: false,
|
||||||
|
is_latest_version_already: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (fetched) {
|
||||||
|
// フェッチ情報のみの場合は、ダウンロードしていない初期状態
|
||||||
|
plugin = {
|
||||||
|
...(prev_plugin || {}),
|
||||||
|
plugin_id: fetched.plugin_id,
|
||||||
|
is_downloaded: false,
|
||||||
|
is_latest_version_available: fetched.is_plugin_supported,
|
||||||
|
is_latest_version_already: false,
|
||||||
|
is_outdated: false,
|
||||||
|
latest_plugin_info: { ...fetched },
|
||||||
|
};
|
||||||
|
} else if (prev_plugin) {
|
||||||
|
// 既存情報のみ存在する場合は outdated フラグを付与
|
||||||
|
plugin = { ...prev_plugin, is_outdated: true };
|
||||||
|
}
|
||||||
|
// いずれかの情報がある場合のみ new_data に追加
|
||||||
|
if (plugin.plugin_id) {
|
||||||
|
new_data.push(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存済み状態(currentSavedPluginsStatus)のマージ
|
||||||
|
// ・new_dataに存在する各プラグインに対して、保存済みの is_enabled を上書き
|
||||||
|
new_data.forEach(plugin => {
|
||||||
|
if (saved_map.has(plugin.plugin_id)) {
|
||||||
|
plugin.is_enabled = saved_map.get(plugin.plugin_id).is_enabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ・prev.data には存在せず、保存済み情報にのみある場合は追加
|
||||||
|
for (const [id, saved] of saved_map.entries()) {
|
||||||
|
if (!new_data.some(item => item.plugin_id === id)) {
|
||||||
|
new_data.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("merged plugin data", new_data);
|
||||||
|
return new_data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
mergePluginData();
|
||||||
|
}, [currentFetchedPluginsInfo.data, currentLoadedPlugins.data, currentSavedPluginsStatus]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- 自動アップデート(ダウンロード処理)---
|
||||||
|
// ※downloadAndExtractPlugin の重複実行を防ぐため、実行中の plugin_id を useRef で管理
|
||||||
|
const downloadingRef = useRef(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentPluginsData.data.length) return;
|
||||||
|
// マージ結果の currentPluginsData.data を元にダウンロード処理をチェック
|
||||||
|
currentPluginsData.data.forEach(plugin => {
|
||||||
|
if (plugin.is_downloaded &&
|
||||||
|
plugin.is_enabled &&
|
||||||
|
!plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||||
|
!plugin.is_latest_version_already &&
|
||||||
|
plugin.is_latest_version_available
|
||||||
|
) {
|
||||||
|
if (!downloadingRef.current.has(plugin.plugin_id)) {
|
||||||
|
showNotification_Success(t("plugin_notifications.updating"));
|
||||||
|
downloadingRef.current.add(plugin.plugin_id);
|
||||||
|
const target_plugin_id = plugin.plugin_id;
|
||||||
|
downloadAndExtractPlugin(plugin)
|
||||||
|
.then(() => {
|
||||||
|
console.log(`Plugin ${target_plugin_id} updated successfully`);
|
||||||
|
downloadingRef.current.delete(target_plugin_id);
|
||||||
|
showNotification_Success(t("plugin_notifications.updated_success"));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`Plugin ${target_plugin_id} update failed`, error);
|
||||||
|
downloadingRef.current.delete(target_plugin_id);
|
||||||
|
showNotification_Error(t("plugin_notifications.updated_error"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [currentPluginsData.data]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化
|
||||||
|
if (store.is_initialized_fetched_plugin_info) {
|
||||||
|
updatePluginsData(prev => {
|
||||||
|
prev.data.forEach(plugin => {
|
||||||
|
if (plugin.is_downloaded && plugin.is_enabled) {
|
||||||
|
if (
|
||||||
|
!plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||||
|
plugin.latest_plugin_info &&
|
||||||
|
!plugin.latest_plugin_info?.is_plugin_supported
|
||||||
|
) {
|
||||||
|
showNotification_Error(t("plugin_notifications.disabled_out_of_support"));
|
||||||
|
plugin.is_enabled = false;
|
||||||
|
setTargetSavedPluginsStatus_Init(plugin.plugin_id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||||
|
plugin.is_outdated
|
||||||
|
) {
|
||||||
|
showNotification_Error(t("plugin_notifications.disabled_out_of_support"));
|
||||||
|
plugin.is_enabled = false;
|
||||||
|
setTargetSavedPluginsStatus_Init(plugin.plugin_id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return prev.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [store.is_initialized_fetched_plugin_info]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
AdvancedSettings,
|
AdvancedSettings,
|
||||||
Vr,
|
Vr,
|
||||||
Hotkeys,
|
Hotkeys,
|
||||||
|
// Plugins,
|
||||||
Supporters,
|
Supporters,
|
||||||
AboutVrct,
|
AboutVrct,
|
||||||
} from "@setting_box";
|
} from "@setting_box";
|
||||||
@@ -32,6 +33,8 @@ export const SettingBox = () => {
|
|||||||
return <Hotkeys />;
|
return <Hotkeys />;
|
||||||
case "advanced_settings":
|
case "advanced_settings":
|
||||||
return <AdvancedSettings />;
|
return <AdvancedSettings />;
|
||||||
|
// case "plugins":
|
||||||
|
// return <Plugins />;
|
||||||
case "supporters":
|
case "supporters":
|
||||||
return <Supporters />;
|
return <Supporters />;
|
||||||
case "about_vrct":
|
case "about_vrct":
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import styles from "./_DownloadButton.module.scss";
|
||||||
|
|
||||||
|
export const _DownloadButton = ({option, ...props}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
const circular_progress = Math.floor(option.progress / 10) * 10;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case option.progress !== null:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
case option.update_button:
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={styles.update_button}
|
||||||
|
onClick={() => props.downloadStartFunction(option.id)}
|
||||||
|
>
|
||||||
|
<p className={styles.download_button_label}>Update</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className={styles.download_container}>{renderContent()}</div>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@import "@scss_mixins";
|
||||||
|
|
||||||
|
.download_container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download_button {
|
||||||
|
pointer-events: auto;
|
||||||
|
background-color: var(--dark_800_color);
|
||||||
|
padding: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--dark_750_color);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--dark_800_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.download_button_label {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress_label {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update_button {
|
||||||
|
pointer-events: auto;
|
||||||
|
background-color: var(--primary_400_color);
|
||||||
|
padding: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary_450_color);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: var(--primary_500_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
|
||||||
import styles from "./DownloadModels.module.scss";
|
|
||||||
import {
|
import {
|
||||||
RadioButton,
|
RadioButton,
|
||||||
} from "../index";
|
} from "../index";
|
||||||
|
import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton";
|
||||||
|
|
||||||
export const DownloadModels = (props) => {
|
export const DownloadModels = (props) => {
|
||||||
const options = props.options.map(item => ({
|
const options = props.options.map(item => ({
|
||||||
@@ -19,47 +17,9 @@ export const DownloadModels = (props) => {
|
|||||||
options={options}
|
options={options}
|
||||||
checked_variable={props.checked_variable}
|
checked_variable={props.checked_variable}
|
||||||
column={true}
|
column={true}
|
||||||
ChildComponent={ModelSelector}
|
ChildComponent={_DownloadButton}
|
||||||
downloadStartFunction={props.downloadStartFunction}
|
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>;
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import styles from "./HotkeysEntry.module.scss";
|
|||||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import DeleteSvg from "@images/cancel.svg?react";
|
import DeleteSvg from "@images/cancel.svg?react";
|
||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const HotkeysEntry = (props) => {
|
export const HotkeysEntry = (props) => {
|
||||||
const [isAcceptingInput, setIsAcceptingInput] = useState(false);
|
const [isAcceptingInput, setIsAcceptingInput] = useState(false);
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export { Slider } from "./slider/Slider";
|
|||||||
export { SwitchBox } from "./switch_box/SwitchBox";
|
export { SwitchBox } from "./switch_box/SwitchBox";
|
||||||
export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
|
export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
|
||||||
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
|
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
|
||||||
export { DownloadModels } from "./download_models/DownloadModels";
|
export { DownloadModels } from "./download_models/DownloadModels";
|
||||||
|
export { PluginsControlComponent } from "./plugins_control_component/PluginsControlComponent";
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { SwitchBox } from "../index";
|
||||||
|
import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton";
|
||||||
|
import styles from "./PluginsControlComponent.module.scss";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const PluginsControlComponent = ({
|
||||||
|
variable_state,
|
||||||
|
plugin_status,
|
||||||
|
toggleFunction,
|
||||||
|
downloadStartFunction,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
id: plugin_status.plugin_id,
|
||||||
|
is_pending: plugin_status.is_pending,
|
||||||
|
is_downloaded: plugin_status.is_downloaded,
|
||||||
|
data: plugin_status.is_enabled,
|
||||||
|
update_button: plugin_status.is_downloaded && plugin_status.is_latest_version_available,
|
||||||
|
state: variable_state,
|
||||||
|
progress: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloaded_version = plugin_status.downloaded_plugin_info?.plugin_version;
|
||||||
|
const latest_version = plugin_status.latest_plugin_info?.plugin_version;
|
||||||
|
|
||||||
|
const downloaded_version_label = t("config_page.plugins.downloaded_version",
|
||||||
|
{ downloaded_version: downloaded_version }
|
||||||
|
);
|
||||||
|
const latest_version_label = t("config_page.plugins.latest_version",
|
||||||
|
{ latest_version: latest_version }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (plugin_status.is_downloaded) {
|
||||||
|
return (
|
||||||
|
<DownloadedPluginControl
|
||||||
|
option={option}
|
||||||
|
plugin_status={plugin_status}
|
||||||
|
toggleFunction={toggleFunction}
|
||||||
|
downloadStartFunction={downloadStartFunction}
|
||||||
|
downloaded_version_label={downloaded_version_label}
|
||||||
|
latest_version_label={latest_version_label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<NotDownloadedPluginControl
|
||||||
|
option={option}
|
||||||
|
plugin_status={plugin_status}
|
||||||
|
downloadStartFunction={downloadStartFunction}
|
||||||
|
downloaded_version_label={downloaded_version_label}
|
||||||
|
latest_version_label={latest_version_label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const DownloadedPluginControl = ({
|
||||||
|
option,
|
||||||
|
plugin_status,
|
||||||
|
toggleFunction,
|
||||||
|
downloadStartFunction,
|
||||||
|
downloaded_version_label,
|
||||||
|
latest_version_label,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const togglePlugin = () => {
|
||||||
|
toggleFunction(plugin_status.plugin_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!plugin_status.downloaded_plugin_info.is_plugin_supported) {
|
||||||
|
if (plugin_status.is_latest_version_available) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{downloaded_version_label}</p>
|
||||||
|
<p>{latest_version_label}</p>
|
||||||
|
<p>{t("config_page.plugins.available_after_updating")}</p>
|
||||||
|
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{t("config_page.plugins.unavailable_downloaded")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (plugin_status.is_outdated) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{t("config_page.plugins.no_latest_info")}</p>
|
||||||
|
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (plugin_status.is_latest_version_already) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{latest_version_label}</p>
|
||||||
|
<p>{t("config_page.plugins.using_latest_version")}</p>
|
||||||
|
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (plugin_status.is_latest_version_available) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{latest_version_label}</p>
|
||||||
|
<p>{t("config_page.plugins.available_latest_version")}</p>
|
||||||
|
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
|
||||||
|
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{t("config_page.plugins.available_latest_version")}</p>
|
||||||
|
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const NotDownloadedPluginControl = ({
|
||||||
|
option,
|
||||||
|
plugin_status,
|
||||||
|
downloadStartFunction,
|
||||||
|
downloaded_version_label,
|
||||||
|
latest_version_label,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (plugin_status.is_latest_version_available) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{latest_version_label}</p>
|
||||||
|
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (plugin_status.latest_plugin_info?.is_plugin_supported_latest_vrct) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{latest_version_label}</p>
|
||||||
|
<p>{t("config_page.plugins.available_in_latest_vrct_version")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p>{latest_version_label}</p>
|
||||||
|
<p>{t("config_page.plugins.unavailable_not_downloaded")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable_text {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./Slider.module.scss";
|
import styles from "./Slider.module.scss";
|
||||||
import MUI_Slider from "@mui/material/Slider";
|
import MUI_Slider from "@mui/material/Slider";
|
||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const Slider = (props) => {
|
export const Slider = (props) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@import "@scss_mixins";
|
@import "@scss_mixins";
|
||||||
|
|
||||||
.switchbox_container {
|
.switchbox_container {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -22,6 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle_control {
|
.toggle_control {
|
||||||
|
position: relative;
|
||||||
@include toggle_control_styles;
|
@include toggle_control_styles;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styles from "./AdvancedSettings.module.scss";
|
import styles from "./AdvancedSettings.module.scss";
|
||||||
|
|
||||||
|
import { Plugins } from "./plugins/Plugins";
|
||||||
|
|
||||||
import { useOpenFolder } from "@logics_common";
|
import { useOpenFolder } from "@logics_common";
|
||||||
import {
|
import {
|
||||||
useOscIpAddress,
|
useOscIpAddress,
|
||||||
@@ -10,22 +12,35 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ActionButtonContainer,
|
ActionButtonContainer,
|
||||||
EntryContainer,
|
|
||||||
EntryWithSaveButtonContainer,
|
EntryWithSaveButtonContainer,
|
||||||
} from "../_templates/Templates";
|
} from "../_templates/Templates";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SectionLabelComponent,
|
||||||
|
} from "../_components/";
|
||||||
|
|
||||||
|
|
||||||
import OpenFolderSvg from "@images/open_folder.svg?react";
|
import OpenFolderSvg from "@images/open_folder.svg?react";
|
||||||
import HelpSvg from "@images/help.svg?react";
|
import HelpSvg from "@images/help.svg?react";
|
||||||
|
|
||||||
export const AdvancedSettings = () => {
|
export const AdvancedSettings = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.container}>
|
||||||
<OscIpAddressContainer />
|
<div>
|
||||||
<OscPortContainer />
|
|
||||||
<OpenConfigFolderContainer />
|
<OscIpAddressContainer />
|
||||||
<OpenSwitchComputeDeviceModalContainer />
|
<OscPortContainer />
|
||||||
</>
|
<OpenConfigFolderContainer />
|
||||||
|
<OpenSwitchComputeDeviceModalContainer />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SectionLabelComponent label={t("config_page.advanced_settings.section_label_plugins")} />
|
||||||
|
<Plugins />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
gap: 6.4rem;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { usePlugins } from "@logics_configs";
|
||||||
|
import styles from "./Plugins.module.scss";
|
||||||
|
import { PluginsControlComponent } from "../../_components/plugins_control_component/PluginsControlComponent";
|
||||||
|
import { useNotificationStatus } from "@logics_common";
|
||||||
|
|
||||||
|
export const Plugins = () => {
|
||||||
|
const {
|
||||||
|
asyncFetchPluginsInfo,
|
||||||
|
} = usePlugins();
|
||||||
|
const hasRunRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasRunRef.current) {
|
||||||
|
asyncFetchPluginsInfo();
|
||||||
|
}
|
||||||
|
return () => hasRunRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<PluginDownloadContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PluginDownloadContainer = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const {
|
||||||
|
downloadAndExtractPlugin,
|
||||||
|
currentPluginsData,
|
||||||
|
currentSavedPluginsStatus,
|
||||||
|
toggleSavedPluginsStatus,
|
||||||
|
handlePendingPlugin,
|
||||||
|
currentFetchedPluginsInfo,
|
||||||
|
} = usePlugins();
|
||||||
|
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
|
||||||
|
|
||||||
|
// ダウンロード開始時の状態更新処理
|
||||||
|
const downloadStartFunction = async (target_plugin_id) => {
|
||||||
|
handlePendingPlugin(target_plugin_id, true);
|
||||||
|
showNotification_Success(t("plugin_notifications.downloading"));
|
||||||
|
|
||||||
|
const target_plugin_info = currentPluginsData.data.find(
|
||||||
|
(d) => d.plugin_id === target_plugin_id
|
||||||
|
);
|
||||||
|
downloadAndExtractPlugin(target_plugin_info).then(() => {
|
||||||
|
handlePendingPlugin(target_plugin_id, false);
|
||||||
|
showNotification_Success(t("plugin_notifications.downloaded_success"));
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
showNotification_Error(t("plugin_notifications.downloaded_error"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// プラグインのオンオフ切り替え処理
|
||||||
|
const toggleFunction = (target_plugin_id) => {
|
||||||
|
toggleSavedPluginsStatus(target_plugin_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const variable_state = currentSavedPluginsStatus.state;
|
||||||
|
|
||||||
|
const filtered_plugins_data = currentPluginsData.data.filter(plugin => !plugin.is_outdated)
|
||||||
|
|
||||||
|
// plugin_id で ABC 順にソート
|
||||||
|
const sorted_plugins_data = filtered_plugins_data.sort((a, b) =>
|
||||||
|
a.plugin_id.localeCompare(b.plugin_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Duplicate
|
||||||
|
const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error";
|
||||||
|
const is_fetching = currentFetchedPluginsInfo.state === "pending";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.plugins_list_container}>
|
||||||
|
{is_failed_to_fetch && <p>Failed to fetch plugins data</p>}
|
||||||
|
{is_fetching && <p>Fetching plugins data...</p>}
|
||||||
|
{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 (
|
||||||
|
<div key={plugin.plugin_id} className={styles.plugin_wrapper}>
|
||||||
|
<div className={styles.labels_wrapper}>
|
||||||
|
<p className={styles.title}>
|
||||||
|
{target_locale.title}
|
||||||
|
</p>
|
||||||
|
<p className={styles.desc}>
|
||||||
|
{target_locale.desc}
|
||||||
|
</p>
|
||||||
|
{/* <p className={styles.plugin_id}>{plugin.plugin_id}</p> */}
|
||||||
|
</div>
|
||||||
|
<div className={styles.plugin_info_wrapper}>
|
||||||
|
{plugin.is_error ? (
|
||||||
|
<p style={{ color: "red" }}>Error: {plugin.error_message}</p>
|
||||||
|
) : (
|
||||||
|
<PluginsControlComponent
|
||||||
|
variable_state={variable_state}
|
||||||
|
toggleFunction={toggleFunction}
|
||||||
|
downloadStartFunction={downloadStartFunction}
|
||||||
|
plugin_status={plugin}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 6.4rem;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins_list_container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin_wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
gap: 2rem;
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 0.1rem solid var(--dark_750_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin_info_wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--dark_500_color);
|
||||||
|
}
|
||||||
|
// .plugin_id {
|
||||||
|
// font-size: 1rem;
|
||||||
|
// color: var(--dark_600_color);
|
||||||
|
// width: 100%;
|
||||||
|
// overflow: hidden;
|
||||||
|
// white-space: nowrap;
|
||||||
|
// text-overflow: ellipsis;
|
||||||
|
// }
|
||||||
@@ -6,5 +6,6 @@ export { Others, VrcMicMuteSyncContainer } from "./others/Others";
|
|||||||
export { AdvancedSettings } from "./advanced_settings/AdvancedSettings";
|
export { AdvancedSettings } from "./advanced_settings/AdvancedSettings";
|
||||||
export { Vr } from "./vr/Vr";
|
export { Vr } from "./vr/Vr";
|
||||||
export { Hotkeys } from "./hotkeys/Hotkeys";
|
export { Hotkeys } from "./hotkeys/Hotkeys";
|
||||||
|
// export { Plugins } from "./plugins/Plugins";
|
||||||
export { AboutVrct } from "./about_vrct/AboutVrct";
|
export { AboutVrct } from "./about_vrct/AboutVrct";
|
||||||
export { Supporters } from "./supporters/Supporters";
|
export { Supporters } from "./supporters/Supporters";
|
||||||
@@ -3,7 +3,7 @@ import fanbox_logo from "@images/supporters/fanbox_logo.png";
|
|||||||
import kofi_logo from "@images/supporters/kofi_logo.png";
|
import kofi_logo from "@images/supporters/kofi_logo.png";
|
||||||
import patreon_logo from "@images/supporters/patreon_logo.png";
|
import patreon_logo from "@images/supporters/patreon_logo.png";
|
||||||
import styles from "./SupportUsContainer.module.scss";
|
import styles from "./SupportUsContainer.module.scss";
|
||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const SupportUsContainer = () => {
|
export const SupportUsContainer = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
import styles from "./Vr.module.scss";
|
import styles from "./Vr.module.scss";
|
||||||
import { ui_configs } from "@ui_configs";
|
import { ui_configs } from "@ui_configs";
|
||||||
import { Slider } from "../_components/";
|
import { Slider } from "../_components/";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const SidebarSection = () => {
|
|||||||
<Tab tab_id="vr" />
|
<Tab tab_id="vr" />
|
||||||
<Tab tab_id="others" />
|
<Tab tab_id="others" />
|
||||||
<Tab tab_id="hotkeys" />
|
<Tab tab_id="hotkeys" />
|
||||||
|
{/* <Tab tab_id="plugins" /> */}
|
||||||
<Tab tab_id="advanced_settings" />
|
<Tab tab_id="advanced_settings" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.separated_tabs_wrapper}>
|
<div className={styles.separated_tabs_wrapper}>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
import styles from "./VersionLabel.module.scss";
|
import styles from "./VersionLabel.module.scss";
|
||||||
|
|
||||||
import { useSoftwareVersion } from "@logics_configs";
|
import { useSoftwareVersion, useComputeMode } from "@logics_common";
|
||||||
import { useComputeMode } from "@logics_common";
|
|
||||||
import CopySvg from "@images/copy.svg?react";
|
import CopySvg from "@images/copy.svg?react";
|
||||||
import CheckMarkSvg from "@images/check_mark.svg?react";
|
import CheckMarkSvg from "@images/check_mark.svg?react";
|
||||||
|
|
||||||
|
|||||||
89
src-ui/app/error_boundary/AppErrorBoundary.jsx
Normal file
89
src-ui/app/error_boundary/AppErrorBoundary.jsx
Normal file
@@ -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 (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallbackRender={({ error }) => (
|
||||||
|
<ErrorContainer error={error} />
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<CloseButtonContainer />
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<p className={styles.error_message}>An error occurred. Please restart VRCT or contact the developers.</p>
|
||||||
|
{error ?
|
||||||
|
<div className={styles.error_detail_container}>
|
||||||
|
<div className={styles.error_stack_container}>
|
||||||
|
<p className={styles.error_stack}>
|
||||||
|
{formatted_stack}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.copy_error_message_button} onClick={copyToClipboard}>
|
||||||
|
<p className={styles.copy_text}>Copy</p>
|
||||||
|
{is_copied
|
||||||
|
? <CheckMarkSvg className={styles.check_mark_svg}/>
|
||||||
|
: <CopySvg className={styles.copy_svg}/>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
<ContactsContainer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CloseButtonContainer = () => {
|
||||||
|
const close = () => {
|
||||||
|
appWindow.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={styles.close_button_wrapper} onClick={close}>
|
||||||
|
<div className={styles.close_button}>
|
||||||
|
<XMarkSvg className={styles.x_mark_svg}/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const formatStackTrace = (stack) => {
|
||||||
|
if (!stack) return "";
|
||||||
|
// フルパスの除去(例として window.location.origin や絶対パス部分を削除)
|
||||||
|
// ※必要に応じて正規表現を調整してください
|
||||||
|
const formatted = stack.replace(new RegExp(window.location.origin, "g"), "");
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
110
src-ui/app/error_boundary/AppErrorBoundary.module.scss
Normal file
110
src-ui/app/error_boundary/AppErrorBoundary.module.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import styles from "./ContactsContainer.module.scss";
|
||||||
|
|
||||||
|
export const ContactsContainer = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<OpenLinkContainer className={styles.github_issues} href_id="github_issues" text="Github Issues"/>
|
||||||
|
<OpenLinkContainer className={styles.google_forms} href_id="google_forms" text="Google Forms"/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<a className={className} href={href} target="_blank" rel="noreferrer" >
|
||||||
|
<img className={styles.contact_button_icon} src={img} />
|
||||||
|
<p className={styles.contact_button_label}>{text}</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,12 +9,27 @@ import { useStore_IsOpenedLanguageSelector } from "@store";
|
|||||||
import { useLanguageSettings } from "@logics_main";
|
import { useLanguageSettings } from "@logics_main";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { PluginHost } from "./PluginHost";
|
||||||
|
|
||||||
|
import { usePlugins } from "@logics_configs";
|
||||||
|
|
||||||
export const MainSection = () => {
|
export const MainSection = () => {
|
||||||
|
const { currentPluginsData } = usePlugins();
|
||||||
|
|
||||||
|
const render_plugins = currentPluginsData.data.filter((plugin) => (
|
||||||
|
plugin.is_downloaded &&
|
||||||
|
plugin.is_enabled &&
|
||||||
|
plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||||
|
plugin.downloaded_plugin_info.location === "main_section"
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<MessageContainer />
|
{render_plugins.length
|
||||||
|
? <PluginHost render_components={render_plugins}/>
|
||||||
|
: <MessageContainer />
|
||||||
|
}
|
||||||
<HandleLanguageSelector />
|
<HandleLanguageSelector />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
// justify-content: space-between;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language_selector_container {
|
.language_selector_container {
|
||||||
|
|||||||
14
src-ui/app/main_page/main_section/PluginHost.jsx
Normal file
14
src-ui/app/main_page/main_section/PluginHost.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const PluginHost = ({render_components}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{render_components
|
||||||
|
.map((plugin, index) => {
|
||||||
|
const PluginComponent = plugin.component;
|
||||||
|
return PluginComponent ? <PluginComponent key={index} /> : null;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import RefreshSvg from "@images/refresh.svg?react";
|
|||||||
import HelpSvg from "@images/help.svg?react";
|
import HelpSvg from "@images/help.svg?react";
|
||||||
|
|
||||||
import { useStore_OpenedQuickSetting } from "@store";
|
import { useStore_OpenedQuickSetting } from "@store";
|
||||||
import { useIsSoftwareUpdateAvailable } from "@logics_common";
|
import { useSoftwareVersion } from "@logics_common";
|
||||||
import { useIsEnabledOverlaySmallLog, useIsEnabledOverlayLargeLog, useEnableVrcMicMuteSync } from "@logics_configs";
|
import { useIsEnabledOverlaySmallLog, useIsEnabledOverlayLargeLog, useEnableVrcMicMuteSync } from "@logics_configs";
|
||||||
import { OpenQuickSettingButton } from "./_buttons/OpenQuickSettingButton";
|
import { OpenQuickSettingButton } from "./_buttons/OpenQuickSettingButton";
|
||||||
|
|
||||||
@@ -66,9 +66,9 @@ const OpenVrcMicMuteSyncQuickSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SoftwareUpdateAvailableButton = () => {
|
const SoftwareUpdateAvailableButton = () => {
|
||||||
const { currentIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable();
|
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (currentIsSoftwareUpdateAvailable.data === false) return null;
|
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
|
||||||
|
|
||||||
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import styles from "./UpdateModal.module.scss";
|
import styles from "./UpdateModal.module.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useStore_OpenedQuickSetting } from "@store";
|
import { useStore_OpenedQuickSetting } from "@store";
|
||||||
import { useComputeMode, useUpdateSoftware } from "@logics_common";
|
import { usePlugins } from "@logics_configs";
|
||||||
import { useIsSoftwareUpdating, useIsSoftwareUpdateAvailable } from "@logics_common";
|
import {
|
||||||
import clsx from "clsx";
|
useComputeMode,
|
||||||
|
useUpdateSoftware,
|
||||||
|
useIsSoftwareUpdating,
|
||||||
|
useSoftwareVersion,
|
||||||
|
} from "@logics_common";
|
||||||
|
|
||||||
|
import { PluginCompatibilityList } from "./plugins_compatibility_list/PluginCompatibilityList";
|
||||||
|
|
||||||
export const UpdateModal = () => {
|
export const UpdateModal = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -11,9 +18,10 @@ export const UpdateModal = () => {
|
|||||||
const { updateSoftware, updateSoftware_CUDA } = useUpdateSoftware();
|
const { updateSoftware, updateSoftware_CUDA } = useUpdateSoftware();
|
||||||
const { updateIsSoftwareUpdating } = useIsSoftwareUpdating();
|
const { updateIsSoftwareUpdating } = useIsSoftwareUpdating();
|
||||||
const { currentComputeMode } = useComputeMode();
|
const { currentComputeMode } = useComputeMode();
|
||||||
const { currentIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable();
|
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||||
|
const { isAnyPluginEnabled } = usePlugins();
|
||||||
|
|
||||||
const is_latest_version_already = currentIsSoftwareUpdateAvailable.data === false;
|
const is_latest_version_already = currentLatestSoftwareVersionInfo.data.is_update_available === false;
|
||||||
const is_cpu_version = currentComputeMode.data === "cpu";
|
const is_cpu_version = currentComputeMode.data === "cpu";
|
||||||
|
|
||||||
const onClickUpdateSoftware = () => {
|
const onClickUpdateSoftware = () => {
|
||||||
@@ -37,30 +45,34 @@ export const UpdateModal = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.update_section}>
|
<div className={styles.update_section_wrapper}>
|
||||||
<div className={styles.cpu_section}>
|
{isAnyPluginEnabled() && <PluginCompatibilityList />}
|
||||||
<div className={styles.button_wrapper}>
|
<div className={styles.update_section}>
|
||||||
<button className={cpu_accept_button_class_name} onClick={onClickUpdateSoftware}>CPU</button>
|
<div className={styles.cpu_section}>
|
||||||
{is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} /> : null}
|
<div className={styles.button_wrapper}>
|
||||||
|
<button className={cpu_accept_button_class_name} onClick={onClickUpdateSoftware}>CPU</button>
|
||||||
|
{is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} /> : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.version_desc_container}>
|
||||||
|
<VersionDescComponent desc={t("update_modal.cpu_desc")} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.version_desc_container}>
|
<div className={styles.cuda_section}>
|
||||||
<VersionDescComponent desc={t("update_modal.cpu_desc")} />
|
<div className={styles.button_wrapper}>
|
||||||
|
<button className={cuda_accept_button_class_name} onClick={onClickUpdateSoftware_CUDA}>CUDA (GPU)</button>
|
||||||
|
{!is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} is_cuda={true}/> : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.version_desc_container}>
|
||||||
|
<VersionDescComponent desc={t("update_modal.cuda_desc")} />
|
||||||
|
<VersionDescComponent desc={t("update_modal.cuda_compare_cpu_desc")} />
|
||||||
|
<VersionDescComponent desc={t("update_modal.cuda_disk_space_desc", {size: "5GB"})} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={styles.cuda_section}>
|
|
||||||
<div className={styles.button_wrapper}>
|
|
||||||
<button className={cuda_accept_button_class_name} onClick={onClickUpdateSoftware_CUDA}>CUDA (GPU)</button>
|
|
||||||
{!is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} is_cuda={true}/> : null}
|
|
||||||
</div>
|
|
||||||
<div className={styles.version_desc_container}>
|
|
||||||
<VersionDescComponent desc={t("update_modal.cuda_desc")} />
|
|
||||||
<VersionDescComponent desc={t("update_modal.cuda_compare_cpu_desc")} />
|
|
||||||
<VersionDescComponent desc={t("update_modal.cuda_disk_space_desc", {size: "5GB"})} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={styles.update_desc}>{t("update_modal.download_latest_and_restart")}</p>
|
<p className={styles.update_desc}>{t("update_modal.download_latest_and_restart")}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.button_wrapper}>
|
<div className={styles.button_wrapper}>
|
||||||
<button className={styles.deny_button} onClick={() => updateOpenedQuickSetting("")} >{t("update_modal.close_modal")}</button>
|
<button className={styles.deny_button} onClick={() => updateOpenedQuickSetting("")} >{t("update_modal.close_modal")}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: safe center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2.4rem;
|
gap: 2.4rem;
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,14 @@
|
|||||||
gap: 8rem;
|
gap: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update_section_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
.update_section {
|
.update_section {
|
||||||
border: 0.1rem solid var(--dark_600_color);
|
border: 0.1rem solid var(--dark_600_color);
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import styles from "./PluginCompatibilityList.module.scss";
|
||||||
|
import { usePlugins } from "@logics_configs";
|
||||||
|
import CheckMarkSvg from "@images/check_mark.svg?react";
|
||||||
|
import XSvg from "@images/x_mark.svg?react";
|
||||||
|
import WarningSvg from "@images/warning.svg?react";
|
||||||
|
|
||||||
|
export const PluginCompatibilityList = () => {
|
||||||
|
const {
|
||||||
|
enabledPluginsList,
|
||||||
|
asyncFetchPluginsInfo,
|
||||||
|
currentFetchedPluginsInfo,
|
||||||
|
} = usePlugins();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
asyncFetchPluginsInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ダウンロード済みのもの
|
||||||
|
const downloaded_plugin = enabledPluginsList().filter(p => p.is_downloaded);
|
||||||
|
|
||||||
|
const compatible_plugins_list = [];
|
||||||
|
const incompatible_plugins_list = [];
|
||||||
|
for (const p of downloaded_plugin) {
|
||||||
|
if (!p.downloaded_plugin_info?.is_plugin_supported_latest_vrct && !p.latest_plugin_info?.is_plugin_supported_latest_vrct) {
|
||||||
|
// プラグイン最新版でも、VRCT最新版(VRCTアプデ後)に非対応のもの
|
||||||
|
incompatible_plugins_list.push(p);
|
||||||
|
} else {
|
||||||
|
// 現プラグイン or 最新版が、VRCT最新版(VRCTアプデ後)に対応しているもの
|
||||||
|
compatible_plugins_list.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_any_incompatible_plugin = incompatible_plugins_list.length > 0;
|
||||||
|
const is_any_compatible_plugin = compatible_plugins_list.length > 0;
|
||||||
|
|
||||||
|
if (!is_any_incompatible_plugin && !is_any_compatible_plugin) return null; // This is just for safety.
|
||||||
|
|
||||||
|
// Duplicate
|
||||||
|
const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error";
|
||||||
|
const is_fetching = currentFetchedPluginsInfo.state === "pending";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p className={styles.title}>使用中プラグインの互換性チェック</p>
|
||||||
|
{is_failed_to_fetch && <p>Failed to fetch plugins data</p>}
|
||||||
|
{is_fetching && <p>Fetching plugins data...</p>}
|
||||||
|
<div className={styles.plugins_compatibility_container}>
|
||||||
|
{incompatible_plugins_list.map(plugin => {
|
||||||
|
const target_data = plugin.downloaded_plugin_info;
|
||||||
|
return <PluginContainer key={target_data.plugin_id} target_data={target_data} is_compatible={false}/>;
|
||||||
|
})}
|
||||||
|
{compatible_plugins_list.map(plugin => {
|
||||||
|
const target_data = plugin.downloaded_plugin_info;
|
||||||
|
return <PluginContainer key={target_data.plugin_id} target_data={target_data} is_compatible={true} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{is_any_incompatible_plugin &&
|
||||||
|
<div className={styles.warning_container}>
|
||||||
|
<WarningSvg className={styles.warning_svg}/>
|
||||||
|
<p className={styles.warning_text}>VRCT最新バージョンで互換性のないプラグインはアップデート後に無効化されます。引き続き使用したい場合は、各プラグインの更新を待ってください。</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PluginContainer = ({ target_data, is_compatible }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.plugin_box}>
|
||||||
|
<p className={clsx(styles.plugin_label, {[styles.is_compatible]: is_compatible})} >{target_data.title}</p>
|
||||||
|
{is_compatible
|
||||||
|
? <CheckMarkSvg className={styles.check_mark_svg}/>
|
||||||
|
: <XSvg className={styles.x_svg}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import Slide from "@mui/material/Slide";
|
import Slide from "@mui/material/Slide";
|
||||||
|
|
||||||
|
|||||||
BIN
src-ui/assets/document.png
Normal file
BIN
src-ui/assets/document.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
1
src-ui/assets/x_mark.svg
Normal file
1
src-ui/assets/x_mark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23.954 21.03l-9.184-9.095 9.092-9.174-2.832-2.807-9.09 9.179-9.176-9.088-2.81 2.81 9.186 9.105-9.095 9.184 2.81 2.81 9.112-9.192 9.18 9.1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 217 B |
@@ -1,4 +1,4 @@
|
|||||||
import { clsx } from "clsx";
|
import clsx from "clsx";
|
||||||
import styles from "./Checkbox.module.scss";
|
import styles from "./Checkbox.module.scss";
|
||||||
export const Checkbox = ({
|
export const Checkbox = ({
|
||||||
checkboxId,
|
checkboxId,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
export { useSoftwareVersion } from "./useSoftwareVersion";
|
||||||
export { useComputeMode } from "./useComputeMode";
|
export { useComputeMode } from "./useComputeMode";
|
||||||
export { useInitProgress } from "./useInitProgress";
|
export { useInitProgress } from "./useInitProgress";
|
||||||
export { useIsBackendReady } from "./useIsBackendReady";
|
export { useIsBackendReady } from "./useIsBackendReady";
|
||||||
export { useWindow } from "./useWindow";
|
export { useWindow } from "./useWindow";
|
||||||
export { useIsOpenedConfigPage } from "./useIsOpenedConfigPage";
|
export { useIsOpenedConfigPage } from "./useIsOpenedConfigPage";
|
||||||
export { useIsSoftwareUpdateAvailable } from "./useIsSoftwareUpdateAvailable";
|
|
||||||
export { useIsSoftwareUpdating } from "./useIsSoftwareUpdating";
|
export { useIsSoftwareUpdating } from "./useIsSoftwareUpdating";
|
||||||
export { useNotificationStatus } from "./useNotificationStatus";
|
export { useNotificationStatus } from "./useNotificationStatus";
|
||||||
export { useOpenFolder } from "./useOpenFolder";
|
export { useOpenFolder } from "./useOpenFolder";
|
||||||
@@ -11,4 +11,5 @@ export { useMessage } from "./useMessage";
|
|||||||
export { useUpdateSoftware } from "./useUpdateSoftware";
|
export { useUpdateSoftware } from "./useUpdateSoftware";
|
||||||
export { useVolume } from "./useVolume";
|
export { useVolume } from "./useVolume";
|
||||||
export { useHandleNetworkConnection } from "./useHandleNetworkConnection";
|
export { useHandleNetworkConnection } from "./useHandleNetworkConnection";
|
||||||
export { useIsVrctAvailable } from "./useIsVrctAvailable";
|
export { useIsVrctAvailable } from "./useIsVrctAvailable";
|
||||||
|
export { useFetch } from "./useFetch";
|
||||||
21
src-ui/logics/common/useFetch.js
Normal file
21
src-ui/logics/common/useFetch.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
||||||
|
|
||||||
|
export const useFetch = () => {
|
||||||
|
const asyncTauriFetchGithub = async (url) => {
|
||||||
|
console.log("tauriFetch", url);
|
||||||
|
|
||||||
|
const release_response = await tauriFetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
responseType: ResponseType.Json,
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"User-Agent": "VRCTPluginApp"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return release_response;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
asyncTauriFetchGithub,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { useStore_IsSoftwareUpdateAvailable } from "@store";
|
|
||||||
|
|
||||||
export const useIsSoftwareUpdateAvailable = () => {
|
|
||||||
const { currentIsSoftwareUpdateAvailable, updateIsSoftwareUpdateAvailable } = useStore_IsSoftwareUpdateAvailable();
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentIsSoftwareUpdateAvailable,
|
|
||||||
updateIsSoftwareUpdateAvailable,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
40
src-ui/logics/common/useSoftwareVersion.js
Normal file
40
src-ui/logics/common/useSoftwareVersion.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -60,6 +60,6 @@ export { useOscPort } from "./advanced_settings/useOscPort";
|
|||||||
|
|
||||||
export { useSupporters } from "./supporters/useSupporters";
|
export { useSupporters } from "./supporters/useSupporters";
|
||||||
|
|
||||||
|
export { usePlugins } from "./plugins/usePlugins";
|
||||||
|
|
||||||
export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition";
|
export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition";
|
||||||
export { useSoftwareVersion } from "./useSoftwareVersion";
|
|
||||||
451
src-ui/logics/configs/plugins/usePlugins.js
Normal file
451
src-ui/logics/configs/plugins/usePlugins.js
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { IS_PLUGIN_PATH_DEV_MODE, getPluginsList } from "@ui_configs";
|
||||||
|
import {
|
||||||
|
store,
|
||||||
|
|
||||||
|
createAtomWithHook,
|
||||||
|
useStore_SavedPluginsStatus,
|
||||||
|
useStore_PluginsData,
|
||||||
|
|
||||||
|
useStore_FetchedPluginsInfo,
|
||||||
|
useStore_LoadedPlugins,
|
||||||
|
} from "@store";
|
||||||
|
import { useStdoutToPython } from "@logics/useStdoutToPython";
|
||||||
|
|
||||||
|
import { transform } from "@babel/standalone";
|
||||||
|
import { writeFile, createDir, exists, removeDir, readDir, BaseDirectory, readTextFile } from "@tauri-apps/api/fs";
|
||||||
|
import { dev_plugins } from "@plugins_index";
|
||||||
|
const imported_dev_plugins = [];
|
||||||
|
dev_plugins.forEach(async ({entry_path}) => {
|
||||||
|
imported_dev_plugins.push({
|
||||||
|
index: await import(`@plugins_path/${entry_path}/index.jsx`),
|
||||||
|
downloaded_plugin_info: await import(`@plugins_path/${entry_path}/plugin_info.json`),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
import JSZip from "jszip";
|
||||||
|
|
||||||
|
import { useFetch, useSoftwareVersion, useNotificationStatus } from "@logics_common";
|
||||||
|
|
||||||
|
import * as logics_configs from "@logics_configs";
|
||||||
|
import * as logics_main from "@logics_main";
|
||||||
|
import * as logics_common from "@logics_common";
|
||||||
|
|
||||||
|
// PLUGIN_LIST_URL は中央リポジトリにある、各プラグインの plugin_info.json への URL の配列を保持する JSON の URL
|
||||||
|
const PLUGIN_LIST_URL = getPluginsList();
|
||||||
|
|
||||||
|
export const usePlugins = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
|
||||||
|
const { asyncStdoutToPython } = useStdoutToPython();
|
||||||
|
|
||||||
|
const { currentFetchedPluginsInfo, updateFetchedPluginsInfo, pendingFetchedPluginsInfo, errorFetchedPluginsInfo } = useStore_FetchedPluginsInfo();
|
||||||
|
const { currentLoadedPlugins, updateLoadedPlugins, pendingLoadedPlugins } = useStore_LoadedPlugins();
|
||||||
|
|
||||||
|
const { currentSavedPluginsStatus, updateSavedPluginsStatus, pendingSavedPluginsStatus } = useStore_SavedPluginsStatus();
|
||||||
|
const { currentPluginsData, updatePluginsData, pendingPluginsData } = useStore_PluginsData();
|
||||||
|
const { checkVrctVerCompatibility } = useSoftwareVersion();
|
||||||
|
|
||||||
|
const { asyncTauriFetchGithub } = useFetch();
|
||||||
|
|
||||||
|
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
const generatePluginContext = (downloaded_plugin_info) => {
|
||||||
|
const plugin_context = {
|
||||||
|
registerComponent: (component) => {
|
||||||
|
if (!downloaded_plugin_info.plugin_id || !downloaded_plugin_info.location || !component) {
|
||||||
|
return console.error("An invalid plugin was detected.", downloaded_plugin_info.plugin_id, downloaded_plugin_info.location, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLoadedPlugins(prev => {
|
||||||
|
const prev_map = new Map(prev.data.map(item => [item.plugin_id, item]));
|
||||||
|
prev_map.set(downloaded_plugin_info.plugin_id, {
|
||||||
|
...downloaded_plugin_info,
|
||||||
|
component: component,
|
||||||
|
});
|
||||||
|
return Array.from(prev_map.values());
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
createAtomWithHook: (...args) => createAtomWithHook(...args),
|
||||||
|
logics: { ...logics_common, ...logics_configs, ...logics_main },
|
||||||
|
i18n: i18n,
|
||||||
|
};
|
||||||
|
return plugin_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncLoadPlugin = async (plugin_folder_relative_path) => {
|
||||||
|
const init_path = "plugins/" + plugin_folder_relative_path + "/index.esm.js";
|
||||||
|
const downloaded_plugin_info_path = "plugins/" + plugin_folder_relative_path + "/plugin_info.json";
|
||||||
|
const plugin_css_path = "plugins/" + plugin_folder_relative_path + "/main.css";
|
||||||
|
try {
|
||||||
|
const downloaded_plugin_info_json = await readTextFile(downloaded_plugin_info_path, { dir: BaseDirectory.Resource, recursive: true });
|
||||||
|
const downloaded_plugin_info = JSON.parse(downloaded_plugin_info_json);
|
||||||
|
|
||||||
|
const plugin_code = await readTextFile(init_path, { dir: BaseDirectory.Resource, recursive: true });
|
||||||
|
const cleaned_code = removeImportStatements(plugin_code);
|
||||||
|
const transpiled_code = transform(cleaned_code, {
|
||||||
|
presets: [
|
||||||
|
["env", { modules: false }],
|
||||||
|
"react",
|
||||||
|
],
|
||||||
|
sourceType: "module"
|
||||||
|
}).code;
|
||||||
|
const blob = new Blob([transpiled_code], { type: "text/javascript" });
|
||||||
|
const blob_url = URL.createObjectURL(blob);
|
||||||
|
const plugin_module = await import(/* @vite-ignore */ blob_url);
|
||||||
|
URL.revokeObjectURL(blob_url);
|
||||||
|
|
||||||
|
if (plugin_module && plugin_module.init) {
|
||||||
|
plugin_module.init(generatePluginContext(downloaded_plugin_info));
|
||||||
|
}
|
||||||
|
await loadPluginCSS(plugin_css_path);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load plugin from", plugin_folder_relative_path, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const asyncLoadAllPlugins = async () => {
|
||||||
|
if (IS_PLUGIN_PATH_DEV_MODE) {
|
||||||
|
imported_dev_plugins.forEach(({ index, downloaded_plugin_info }) => {
|
||||||
|
if (!index || !downloaded_plugin_info) {
|
||||||
|
console.error("Invalid development plugin detected", index, downloaded_plugin_info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const plugin_context = generatePluginContext(downloaded_plugin_info);
|
||||||
|
if (index.init) {
|
||||||
|
index.init(plugin_context);
|
||||||
|
} else {
|
||||||
|
console.error("Plugin missing init function", downloaded_plugin_info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const is_plugins_dir_exists = await exists("plugins", { dir: BaseDirectory.Resource });
|
||||||
|
if (!is_plugins_dir_exists) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plugin_entries = await readDir("plugins", { dir: BaseDirectory.Resource, recursive: true });
|
||||||
|
const plugin_files = plugin_entries.filter(entry => entry.children && Array.isArray(entry.children));
|
||||||
|
|
||||||
|
for (const target_dir of plugin_files) {
|
||||||
|
const target_path = target_dir.name;
|
||||||
|
await asyncLoadPlugin(target_path);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading plugins:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadAndExtractPlugin = async (plugin) => {
|
||||||
|
const latest_plugin_info = plugin.latest_plugin_info;
|
||||||
|
try {
|
||||||
|
const plugin_zip_url = await fetchLatestPluginZipUrl(latest_plugin_info);
|
||||||
|
console.log("start download", plugin_zip_url);
|
||||||
|
// Rust コマンド経由で ZIP をダウンロード
|
||||||
|
const base64_zip = await invoke("download_zip_asset", { url: plugin_zip_url });
|
||||||
|
// base64_zip をデコードして Uint8Array に変換
|
||||||
|
const binary_string = atob(base64_zip);
|
||||||
|
const len = binary_string.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSZip で ZIP を解凍
|
||||||
|
const zip = await JSZip.loadAsync(bytes);
|
||||||
|
|
||||||
|
// 展開先ディレクトリのパス(例:"plugins/<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 });
|
||||||
|
}
|
||||||
|
await createDir(target_plugin_path, { dir: BaseDirectory.Resource, recursive: true });
|
||||||
|
|
||||||
|
const file_promises = [];
|
||||||
|
zip.forEach((relative_path, zip_entry) => {
|
||||||
|
// .git 以下のファイルはスキップ
|
||||||
|
if (relative_path.startsWith(".git") || relative_path.includes("/.git/")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file_path = target_plugin_path + "/" + relative_path;
|
||||||
|
if (zip_entry.dir) {
|
||||||
|
file_promises.push(
|
||||||
|
createDir(file_path, { dir: BaseDirectory.Resource, recursive: true }).catch((err) => {
|
||||||
|
if (!err.message?.includes("already exists")) {
|
||||||
|
console.error("Failed to create directory:", file_path, err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const dir_path = file_path.substring(0, file_path.lastIndexOf("/"));
|
||||||
|
const promise = createDir(dir_path, { dir: BaseDirectory.Resource, recursive: true })
|
||||||
|
.catch((err) => {
|
||||||
|
if (!err.message?.includes("already exists")) {
|
||||||
|
console.error("Failed to create parent directory:", dir_path, err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => zip_entry.async("text"))
|
||||||
|
.then(async (file_data) => {
|
||||||
|
await writeFile(file_path, file_data, { dir: BaseDirectory.Resource, recursive: true });
|
||||||
|
});
|
||||||
|
file_promises.push(promise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(file_promises);
|
||||||
|
console.log("Plugin downloaded successfully.");
|
||||||
|
|
||||||
|
const index_file_relative_path = plugin.plugin_id;
|
||||||
|
await asyncLoadPlugin(index_file_relative_path);
|
||||||
|
|
||||||
|
console.log("Plugin loaded successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading and extracting plugin:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLatestPluginZipUrl = async (plugin) => {
|
||||||
|
const api_url = plugin.url;
|
||||||
|
const response = await asyncTauriFetchGithub(api_url);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Failed to fetch latest release info, status: " + response.status);
|
||||||
|
}
|
||||||
|
const release_info = response.data;
|
||||||
|
const asset = release_info.assets.find((a) => a.name === plugin.asset_name);
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(`Asset ${plugin.asset_name} not found in the latest release`);
|
||||||
|
}
|
||||||
|
return asset.browser_download_url;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const asyncFetchPluginsInfo = async () => {
|
||||||
|
if (store.is_fetched_plugins_info_already) return;
|
||||||
|
store.is_fetched_plugins_info_already = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await asyncTauriFetchGithub(PLUGIN_LIST_URL);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Failed to fetch plugins list, status: " + response.status);
|
||||||
|
}
|
||||||
|
const plugins_data = response.data;
|
||||||
|
const updated_list = await Promise.all(
|
||||||
|
plugins_data.map(async (plugin_data) => {
|
||||||
|
try {
|
||||||
|
const plugin_info = await asyncFetchPluginInfo(plugin_data.url);
|
||||||
|
return plugin_info;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching plugin info for URL: ", plugin_data.url, error);
|
||||||
|
return {
|
||||||
|
title: plugin_data.title,
|
||||||
|
plugin_id: plugin_data.plugin_id || plugin_data.title,
|
||||||
|
is_error: true,
|
||||||
|
error_message: error.message,
|
||||||
|
url: plugin_data.url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
updateFetchedPluginsInfo(updated_list);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching plugin info list: ", error);
|
||||||
|
errorFetchedPluginsInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
store.is_initialized_fetched_plugin_info = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncFetchPluginInfo = async (plugin_info_asset_url) => {
|
||||||
|
|
||||||
|
const release_response = await asyncTauriFetchGithub(plugin_info_asset_url);
|
||||||
|
if (release_response.status !== 200) {
|
||||||
|
throw new Error(`Failed to fetch release info from ${plugin_info_asset_url}`);
|
||||||
|
}
|
||||||
|
const plugin_info_json = release_response.data.assets.find(asset => asset.name === "plugin_info.json");
|
||||||
|
if (!plugin_info_json) {
|
||||||
|
throw new Error("plugin_info.json not found in release assets");
|
||||||
|
}
|
||||||
|
const plugin_info_json_response = await asyncTauriFetchGithub(plugin_info_json.browser_download_url);
|
||||||
|
if (plugin_info_json_response.status !== 200) {
|
||||||
|
throw new Error(`Failed to fetch plugin_info.json from ${plugin_info_json.browser_download_url}`);
|
||||||
|
}
|
||||||
|
const plugin_info = plugin_info_json_response.data;
|
||||||
|
|
||||||
|
|
||||||
|
const { is_plugin_supported, is_plugin_supported_latest_vrct } = checkVrctVerCompatibility(plugin_info.min_supported_vrct_version, plugin_info.max_supported_vrct_version);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...plugin_info,
|
||||||
|
is_plugin_supported: is_plugin_supported,
|
||||||
|
is_plugin_supported_latest_vrct: is_plugin_supported_latest_vrct,
|
||||||
|
url: plugin_info_asset_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePendingPlugin = (target_plugin_id, is_pending) => {
|
||||||
|
updatePluginsData((old_value) => {
|
||||||
|
const new_value = old_value.data.map((d) => {
|
||||||
|
if (d.plugin_id === target_plugin_id) {
|
||||||
|
d.is_pending = is_pending;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
return new_value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSavedPluginsStatus = (target_plugin_id) => {
|
||||||
|
const is_exists = currentSavedPluginsStatus.data.some(
|
||||||
|
(d) => d.plugin_id === target_plugin_id
|
||||||
|
);
|
||||||
|
let new_value = [];
|
||||||
|
if (is_exists) {
|
||||||
|
new_value = currentSavedPluginsStatus.data.map((d) => {
|
||||||
|
if (d.plugin_id === target_plugin_id) {
|
||||||
|
d.is_enabled = !d.is_enabled;
|
||||||
|
(d.is_enabled)
|
||||||
|
? showNotification_Success(t("plugin_notifications.is_enabled"))
|
||||||
|
: showNotification_Success(t("plugin_notifications.is_disabled"));
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new_value.push(...currentSavedPluginsStatus.data);
|
||||||
|
new_value.push({
|
||||||
|
plugin_id: target_plugin_id,
|
||||||
|
is_enabled: true,
|
||||||
|
});
|
||||||
|
showNotification_Success(t("plugin_notifications.is_enabled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「currentPluginsData.data」でis_downloadedがtrueのものだけ残す
|
||||||
|
new_value = new_value.filter((item) =>
|
||||||
|
currentPluginsData.data.some(
|
||||||
|
(plugin) => plugin.plugin_id === item.plugin_id && plugin.is_downloaded
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setSavedPluginsStatus(new_value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Init時の処理 非対応のものを無効化する際に、savedDPluginsStatusから不要なものを削除する処理が邪魔になるので該当コードを削除したバージョン。Init以外で使用する時にはリファクタが必要になる。
|
||||||
|
const setTargetSavedPluginsStatus_Init = (target_plugin_id, is_enabled) => {
|
||||||
|
const is_exists = currentSavedPluginsStatus.data.some(
|
||||||
|
(d) => d.plugin_id === target_plugin_id
|
||||||
|
);
|
||||||
|
let new_value = [];
|
||||||
|
if (is_exists) {
|
||||||
|
new_value = currentSavedPluginsStatus.data.map((d) => {
|
||||||
|
if (d.plugin_id === target_plugin_id) {
|
||||||
|
d.is_enabled = is_enabled;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
new_value.push(...currentSavedPluginsStatus.data);
|
||||||
|
new_value.push({
|
||||||
|
plugin_id: target_plugin_id,
|
||||||
|
is_enabled: is_enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavedPluginsStatus(new_value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const setSavedPluginsStatus = (plugins_status) => {
|
||||||
|
pendingSavedPluginsStatus();
|
||||||
|
asyncStdoutToPython("/set/data/plugins_status", plugins_status);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init時、currentPluginsDataからのデータではデータ更新が間に合わないので、currentSavedPluginsStatusから直接取得
|
||||||
|
const isAnyPluginEnabled_Init = () => {
|
||||||
|
return currentSavedPluginsStatus.data.some(plugin => plugin.is_enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAnyPluginEnabled = () => {
|
||||||
|
return currentPluginsData.data.some(plugin => plugin.is_enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabledPluginsList = () => {
|
||||||
|
return currentPluginsData.data.filter(plugin => plugin.is_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTargetPluginData = (target_plugin_id, attribute, value) => {
|
||||||
|
updatePluginsData(prev => {
|
||||||
|
prev.data.forEach(plugin => {
|
||||||
|
if (plugin.plugin_id === target_plugin_id) {
|
||||||
|
plugin[attribute] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return prev.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
asyncFetchPluginsInfo,
|
||||||
|
|
||||||
|
isAnyPluginEnabled_Init,
|
||||||
|
isAnyPluginEnabled,
|
||||||
|
enabledPluginsList,
|
||||||
|
|
||||||
|
asyncLoadAllPlugins,
|
||||||
|
downloadAndExtractPlugin,
|
||||||
|
|
||||||
|
currentSavedPluginsStatus,
|
||||||
|
updateSavedPluginsStatus,
|
||||||
|
|
||||||
|
currentPluginsData,
|
||||||
|
updatePluginsData,
|
||||||
|
|
||||||
|
updateTargetPluginData,
|
||||||
|
|
||||||
|
currentFetchedPluginsInfo,
|
||||||
|
updateFetchedPluginsInfo,
|
||||||
|
|
||||||
|
currentLoadedPlugins,
|
||||||
|
updateLoadedPlugins,
|
||||||
|
|
||||||
|
toggleSavedPluginsStatus,
|
||||||
|
setTargetSavedPluginsStatus_Init,
|
||||||
|
setSavedPluginsStatus,
|
||||||
|
|
||||||
|
handlePendingPlugin,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeImportStatements = (code) => {
|
||||||
|
return code
|
||||||
|
.split("\n")
|
||||||
|
.filter(line => !line.match(/^import\s+.*['"]react['"]/))
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
// import { readTextFile, BaseDirectory } from "@tauri-apps/api/fs";
|
||||||
|
|
||||||
|
const loadPluginCSS = async (plugin_css_path) => {
|
||||||
|
if (!await exists(plugin_css_path, { dir: BaseDirectory.Resource, recursive: true })) return;
|
||||||
|
try {
|
||||||
|
// プラグインフォルダのルートにある main.css を読み込む
|
||||||
|
const css_content = await readTextFile(plugin_css_path, { dir: BaseDirectory.Resource });
|
||||||
|
|
||||||
|
// style タグを作成して head に挿入する
|
||||||
|
const style_tag = document.createElement("style");
|
||||||
|
style_tag.id = `plugin-css-${plugin_css_path.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||||
|
style_tag.textContent = css_content;
|
||||||
|
document.head.appendChild(style_tag);
|
||||||
|
console.log("Plugin CSS loaded for:", plugin_css_path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load plugin CSS from", plugin_css_path, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { loadPluginCSS };
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -8,13 +8,13 @@ import {
|
|||||||
useNotificationStatus,
|
useNotificationStatus,
|
||||||
useHandleNetworkConnection,
|
useHandleNetworkConnection,
|
||||||
|
|
||||||
|
useSoftwareVersion,
|
||||||
useComputeMode,
|
useComputeMode,
|
||||||
useInitProgress,
|
useInitProgress,
|
||||||
useIsBackendReady,
|
useIsBackendReady,
|
||||||
useWindow,
|
useWindow,
|
||||||
useMessage,
|
useMessage,
|
||||||
useVolume,
|
useVolume,
|
||||||
useIsSoftwareUpdateAvailable,
|
|
||||||
} from "@logics_common";
|
} from "@logics_common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
} from "@logics_main";
|
} from "@logics_main";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useSoftwareVersion,
|
|
||||||
useEnableAutoMicSelect,
|
useEnableAutoMicSelect,
|
||||||
useEnableAutoSpeakerSelect,
|
useEnableAutoSpeakerSelect,
|
||||||
useMicHostList,
|
useMicHostList,
|
||||||
@@ -73,6 +72,7 @@ import {
|
|||||||
useOverlayShowOnlyTranslatedMessages,
|
useOverlayShowOnlyTranslatedMessages,
|
||||||
useEnableNotificationVrcSfx,
|
useEnableNotificationVrcSfx,
|
||||||
useHotkeys,
|
useHotkeys,
|
||||||
|
usePlugins,
|
||||||
useOscIpAddress,
|
useOscIpAddress,
|
||||||
useOscPort,
|
useOscPort,
|
||||||
} from "@logics_configs";
|
} from "@logics_configs";
|
||||||
@@ -104,7 +104,7 @@ export const useReceiveRoutes = () => {
|
|||||||
addSentMessageLog,
|
addSentMessageLog,
|
||||||
addReceivedMessageLog,
|
addReceivedMessageLog,
|
||||||
} = useMessage();
|
} = useMessage();
|
||||||
const { updateIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable();
|
const { updateLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||||
const { updateSoftwareVersion } = useSoftwareVersion();
|
const { updateSoftwareVersion } = useSoftwareVersion();
|
||||||
const { updateEnableAutoMicSelect } = useEnableAutoMicSelect();
|
const { updateEnableAutoMicSelect } = useEnableAutoMicSelect();
|
||||||
const { updateEnableAutoSpeakerSelect } = useEnableAutoSpeakerSelect();
|
const { updateEnableAutoSpeakerSelect } = useEnableAutoSpeakerSelect();
|
||||||
@@ -176,6 +176,7 @@ export const useReceiveRoutes = () => {
|
|||||||
const { updateEnableNotificationVrcSfx } = useEnableNotificationVrcSfx();
|
const { updateEnableNotificationVrcSfx } = useEnableNotificationVrcSfx();
|
||||||
|
|
||||||
const { updateHotkeys } = useHotkeys();
|
const { updateHotkeys } = useHotkeys();
|
||||||
|
const { updateSavedPluginsStatus } = usePlugins();
|
||||||
|
|
||||||
const { updateOscIpAddress } = useOscIpAddress();
|
const { updateOscIpAddress } = useOscIpAddress();
|
||||||
const { updateOscPort } = useOscPort();
|
const { updateOscPort } = useOscPort();
|
||||||
@@ -205,7 +206,12 @@ export const useReceiveRoutes = () => {
|
|||||||
"/set/data/main_window_geometry": () => {},
|
"/set/data/main_window_geometry": () => {},
|
||||||
"/run/open_filepath_logs": () => console.log("Opened Directory, Message Logs"),
|
"/run/open_filepath_logs": () => console.log("Opened Directory, Message Logs"),
|
||||||
"/run/open_filepath_config_file": () => console.log("Opened Directory, Config File"),
|
"/run/open_filepath_config_file": () => console.log("Opened Directory, Config File"),
|
||||||
"/run/update_software_flag": updateIsSoftwareUpdateAvailable,
|
"/run/software_update_info": (payload) => {
|
||||||
|
updateLatestSoftwareVersionInfo(prev => ({
|
||||||
|
is_update_available: payload.is_update_available,
|
||||||
|
new_version: payload.new_version || prev.data.new_version,
|
||||||
|
}));
|
||||||
|
},
|
||||||
"/run/connected_network": handleNetworkConnection,
|
"/run/connected_network": handleNetworkConnection,
|
||||||
|
|
||||||
// Main Page
|
// Main Page
|
||||||
@@ -488,6 +494,10 @@ export const useReceiveRoutes = () => {
|
|||||||
"/get/data/hotkeys": updateHotkeys,
|
"/get/data/hotkeys": updateHotkeys,
|
||||||
"/set/data/hotkeys": updateHotkeys,
|
"/set/data/hotkeys": updateHotkeys,
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
"/get/data/plugins_status": updateSavedPluginsStatus,
|
||||||
|
"/set/data/plugins_status": updateSavedPluginsStatus,
|
||||||
|
|
||||||
// Advanced Settings
|
// Advanced Settings
|
||||||
"/get/data/osc_ip_address": updateOscIpAddress,
|
"/get/data/osc_ip_address": updateOscIpAddress,
|
||||||
"/set/data/osc_ip_address": updateOscIpAddress,
|
"/set/data/osc_ip_address": updateOscIpAddress,
|
||||||
|
|||||||
25
src-ui/plugins/dev_plugin_subtitles/index.jsx
Normal file
25
src-ui/plugins/dev_plugin_subtitles/index.jsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { initStore, StoreContext } from "@plugin_store";
|
||||||
|
import { initI18n } from "@initI18n";
|
||||||
|
import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer";
|
||||||
|
import { SubtitlesController } from "./subtitle_system_container/_controllers/SubtitlesController.jsx";
|
||||||
|
|
||||||
|
export const init = (plugin_context) => {
|
||||||
|
const { createAtomWithHook, i18n, logics } = plugin_context;
|
||||||
|
|
||||||
|
initStore(createAtomWithHook);
|
||||||
|
initI18n(i18n);
|
||||||
|
|
||||||
|
const EntryComponents = () => {
|
||||||
|
return (
|
||||||
|
<StoreContext.Provider value={logics}>
|
||||||
|
<SubtitlesController />
|
||||||
|
|
||||||
|
<SubtitleSystemContainer />
|
||||||
|
</StoreContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
plugin_context.registerComponent(EntryComponents);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default init;
|
||||||
2
src-ui/plugins/dev_plugin_subtitles/locales/en.yml
Normal file
2
src-ui/plugins/dev_plugin_subtitles/locales/en.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
main_page:
|
||||||
|
title: "VRCT Subtitles"
|
||||||
11
src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js
Normal file
11
src-ui/plugins/dev_plugin_subtitles/locales/initI18n.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import en from "./en.yml";
|
||||||
|
import ja from "./ja.yml";
|
||||||
|
import plugin_info from "../plugin_info.json";
|
||||||
|
|
||||||
|
export const initI18n = (i18n) => {
|
||||||
|
const ns = plugin_info.plugin_id;
|
||||||
|
|
||||||
|
// addResourceBundle will merge into i18n’s store
|
||||||
|
i18n.addResourceBundle("en", ns, en, /* deep = */ true, /* overwrite = */ true);
|
||||||
|
i18n.addResourceBundle("ja", ns, ja, /* deep = */ true, /* overwrite = */ true);
|
||||||
|
};
|
||||||
2
src-ui/plugins/dev_plugin_subtitles/locales/ja.yml
Normal file
2
src-ui/plugins/dev_plugin_subtitles/locales/ja.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
main_page:
|
||||||
|
title: "字幕プレイヤー"
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
7
src-ui/plugins/dev_plugin_subtitles/plugin_configs.js
Normal file
7
src-ui/plugins/dev_plugin_subtitles/plugin_configs.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const configs = {
|
||||||
|
alias: {
|
||||||
|
"@plugin_store": "store/store.js",
|
||||||
|
"@initI18n": "locales/initI18n.js",
|
||||||
|
"@usePluginTranslation": "locales/usePluginTranslation.jsx",
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src-ui/plugins/dev_plugin_subtitles/plugin_info.json
Normal file
20
src-ui/plugins/dev_plugin_subtitles/plugin_info.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"title": "VRCT Subtitles",
|
||||||
|
"desc": "No description",
|
||||||
|
"plugin_id": "vrct_plugin_subtitles",
|
||||||
|
"asset_name": "vrct_plugin_subtitles.zip",
|
||||||
|
"location": "main_section",
|
||||||
|
"plugin_version": "0.0.5",
|
||||||
|
"min_supported_vrct_version": "3.0.5",
|
||||||
|
"max_supported_vrct_version": "3.0.5",
|
||||||
|
"locales": {
|
||||||
|
"en": {
|
||||||
|
"title": "VRCT Subtitles",
|
||||||
|
"desc": "No description"
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"title": "VRCT 字幕表示機能",
|
||||||
|
"desc": "VRCTのオーバーレイ機能を使い、目の前に字幕としてテキストを表示する機能です。ワールドギミックの開始タイミングに合わせて字幕を設定し、同時に表示しているだけではあります。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src-ui/plugins/dev_plugin_subtitles/store/store.js
Normal file
37
src-ui/plugins/dev_plugin_subtitles/store/store.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const store_hooks = {};
|
||||||
|
|
||||||
|
export const initStore = (createAtomWithHook) => {
|
||||||
|
Object.assign(store_hooks, {
|
||||||
|
useStore_IsSubtitlePlaying: createAtomWithHook(false, "IsSubtitlePlaying", { is_state_ok: true }).useHook,
|
||||||
|
useStore_SubtitlePlaybackMode: createAtomWithHook("relative", "SubtitlePlaybackMode", { is_state_ok: true }).useHook,
|
||||||
|
useStore_SubtitleAbsoluteTargetTime: createAtomWithHook({
|
||||||
|
hour: "23",
|
||||||
|
minute: "00",
|
||||||
|
}, "SubtitleAbsoluteTargetTime", { is_state_ok: true }).useHook,
|
||||||
|
useStore_IsCuesScheduled: createAtomWithHook(false, "IsCuesScheduled", { is_state_ok: true }).useHook,
|
||||||
|
useStore_CountdownAdjustment: createAtomWithHook(0, "CountdownAdjustment", { is_state_ok: true }).useHook,
|
||||||
|
useStore_EffectiveCountdown: createAtomWithHook(null, "EffectiveCountdown", { is_state_ok: true }).useHook,
|
||||||
|
useStore_SubtitleCues: createAtomWithHook([], "SubtitleCues", { is_state_ok: true }).useHook,
|
||||||
|
|
||||||
|
useStore_SubtitleTimers: createAtomWithHook([], "SubtitleTimers", { is_state_ok: true }).useHook,
|
||||||
|
useStore_SubtitleCountdownTimerId: createAtomWithHook([], "SubtitleCountdownTimerId", { is_state_ok: true }).useHook,
|
||||||
|
useStore_SubtitleFileName: createAtomWithHook("ファイルが選択されていません", "SubtitleFileName", { is_state_ok: true }).useHook,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStore = (hook_name) => {
|
||||||
|
if (!store_hooks[hook_name]) {
|
||||||
|
throw new Error(`Hook ${hook_name} is not initialized.`);
|
||||||
|
}
|
||||||
|
return store_hooks[hook_name]();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// StoreContext.js
|
||||||
|
import React, { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export const StoreContext = createContext(null);
|
||||||
|
|
||||||
|
export const useStoreContext = () => {
|
||||||
|
return useContext(StoreContext);
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import styles from "./SubtitleSystemContainer.module.scss";
|
||||||
|
import { InputFileContainer } from "./input_file_container/InputFileContainer";
|
||||||
|
import { ModeSelectorContainer } from "./mode_selector_container/ModeSelectorContainer";
|
||||||
|
import { PlayControlContainer } from "./play_control_container/PlayControlContainer";
|
||||||
|
import { CountdownContainer } from "./countdown_container/CountdownContainer";
|
||||||
|
import { SubtitlesListContainer } from "./subtitles_list_container/SubtitlesListContainer";
|
||||||
|
import { usePluginTranslation } from "@usePluginTranslation";
|
||||||
|
|
||||||
|
export const SubtitleSystemContainer = () => {
|
||||||
|
const { t } = usePluginTranslation();
|
||||||
|
// const [srtContent, setSrtContent] = useState("");
|
||||||
|
// const [cues, setCues] = useState([]);
|
||||||
|
// const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
|
||||||
|
// 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から)
|
||||||
|
// const [playbackMode, setPlaybackMode] = useState("relative");
|
||||||
|
// 絶対モード用の再生開始時刻(ドロップダウンで選択、HH:MM)
|
||||||
|
// const [targetHour, setTargetHour] = useState("23");
|
||||||
|
// const [targetMinute, setTargetMinute] = useState("00");
|
||||||
|
|
||||||
|
// カウントダウン状態
|
||||||
|
// // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数
|
||||||
|
// const [initialCountdown, setInitialCountdown] = useState(null);
|
||||||
|
// countdownAdjustment: ユーザーが上下ボタンで調整する値(秒単位)
|
||||||
|
// const [countdownAdjustment, setCountdownAdjustment] = useState(0);
|
||||||
|
// effectiveCountdown: (initialCountdown + countdownAdjustment) から経過秒数を差し引いた表示用の値
|
||||||
|
// const [effectiveCountdown, setEffectiveCountdown] = useState(null);
|
||||||
|
// cuesScheduled: 字幕タイマーが一度スケジュールされたか
|
||||||
|
// const [cuesScheduled, setCuesScheduled] = useState(false);
|
||||||
|
|
||||||
|
// // タイマー(setTimeout/setInterval)のID管理用
|
||||||
|
// const timersRef = useRef([]);
|
||||||
|
// // カウントダウンタイマー専用の ref
|
||||||
|
// const countdownIntervalRef = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h1 className={styles.title}>{t("main_page.title")}</h1>
|
||||||
|
<InputFileContainer />
|
||||||
|
<ModeSelectorContainer />
|
||||||
|
<PlayControlContainer />
|
||||||
|
<div className={styles.border}></div>
|
||||||
|
<CountdownContainer />
|
||||||
|
<SubtitlesListContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
.container {
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
background: var(--dark_900_color);
|
||||||
|
border-radius: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.2rem;
|
||||||
|
background-color: var(--dark_800_color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// label {
|
||||||
|
// display: block;
|
||||||
|
// font-size: 1.6rem;
|
||||||
|
// margin-bottom: 0.5rem;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// input,
|
||||||
|
// select {
|
||||||
|
// font-size: 1.6rem;
|
||||||
|
// padding: 0.5rem;
|
||||||
|
// border-radius: 0.5rem;
|
||||||
|
// border: 0.1rem solid #ccc;
|
||||||
|
// background: #333;
|
||||||
|
// color: #fff;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// ボタンの基本スタイル
|
||||||
|
// button {
|
||||||
|
// // font-size: 1.8rem;
|
||||||
|
// // padding: 1rem 2rem;
|
||||||
|
// // border: none;
|
||||||
|
// // border-radius: 0.5rem;
|
||||||
|
// // cursor: pointer;
|
||||||
|
// // // transition: background 0.3s;
|
||||||
|
// // margin-right: 1rem;
|
||||||
|
|
||||||
|
// &:focus {
|
||||||
|
// outline: none;
|
||||||
|
// box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 再生開始用ボタン(通常時)
|
||||||
|
.primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
&:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再生停止用ボタン
|
||||||
|
.secondary {
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
&:hover {
|
||||||
|
background: #a71d2a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 「再生中」状態(クリック不可)用のスタイル
|
||||||
|
.is_playing {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 字幕一覧のテーブル
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 0.1rem solid #444;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle_lists {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useStoreContext } from "../../store/store.js";
|
||||||
|
|
||||||
|
import { useSubtitles } from "../_logics/useSubtitles";
|
||||||
|
import { secToDayTime } from "../_subtitles_utils"
|
||||||
|
import { useEffect } from "react";
|
||||||
|
export const SubtitlesController = () => {
|
||||||
|
const { useSendTextToOverlay } = useStoreContext();
|
||||||
|
const { sendTextToOverlay } = useSendTextToOverlay();
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentIsSubtitlePlaying,
|
||||||
|
currentIsCuesScheduled,
|
||||||
|
updateIsCuesScheduled,
|
||||||
|
currentCountdownAdjustment,
|
||||||
|
currentEffectiveCountdown,
|
||||||
|
scheduleCues,
|
||||||
|
} = useSubtitles();
|
||||||
|
|
||||||
|
// currentEffectiveCountdown.data が 0 になったとき、字幕開始
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
currentIsSubtitlePlaying.data &&
|
||||||
|
currentEffectiveCountdown.data !== null &&
|
||||||
|
currentEffectiveCountdown.data <= 0 &&
|
||||||
|
!currentIsCuesScheduled.data
|
||||||
|
) {
|
||||||
|
sendTextToOverlay("スタート!");
|
||||||
|
console.log("スタート!");
|
||||||
|
// 調整後のタイミングで字幕スケジュールを開始
|
||||||
|
scheduleCues(0);
|
||||||
|
updateIsCuesScheduled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentEffectiveCountdown.data > 0) {
|
||||||
|
console.log(secToDayTime(currentEffectiveCountdown.data));
|
||||||
|
sendTextToOverlay(secToDayTime(currentEffectiveCountdown.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [currentEffectiveCountdown.data, currentIsSubtitlePlaying.data, currentIsCuesScheduled.data, currentCountdownAdjustment.data]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { useStore, useStoreContext } from "../../store/store.js";
|
||||||
|
|
||||||
|
export const useSubtitles = () => {
|
||||||
|
const { useSendTextToOverlay } = useStoreContext();
|
||||||
|
const { sendTextToOverlay } = useSendTextToOverlay();
|
||||||
|
|
||||||
|
const { currentSubtitleFileName, updateSubtitleFileName } = useStore("useStore_SubtitleFileName");
|
||||||
|
const { currentIsSubtitlePlaying, updateIsSubtitlePlaying } = useStore("useStore_IsSubtitlePlaying");
|
||||||
|
const { currentSubtitlePlaybackMode, updateSubtitlePlaybackMode } = useStore("useStore_SubtitlePlaybackMode");
|
||||||
|
const { currentSubtitleAbsoluteTargetTime, updateSubtitleAbsoluteTargetTime } = useStore("useStore_SubtitleAbsoluteTargetTime");
|
||||||
|
const { currentIsCuesScheduled, updateIsCuesScheduled } = useStore("useStore_IsCuesScheduled");
|
||||||
|
|
||||||
|
const { currentCountdownAdjustment, updateCountdownAdjustment } = useStore("useStore_CountdownAdjustment");
|
||||||
|
const { currentEffectiveCountdown, updateEffectiveCountdown } = useStore("useStore_EffectiveCountdown");
|
||||||
|
const { currentSubtitleCues, updateSubtitleCues } = useStore("useStore_SubtitleCues");
|
||||||
|
|
||||||
|
// タイマー(setTimeout/setInterval)のID管理用
|
||||||
|
const { currentSubtitleTimers, updateSubtitleTimers, addSubtitleTimers } = useStore("useStore_SubtitleTimers");
|
||||||
|
// const timersRef = useRef([]);
|
||||||
|
// カウントダウンタイマー専用の ref
|
||||||
|
const { currentSubtitleCountdownTimerId, updateSubtitleCountdownTimerId, AddSubtitleCountdownTimerId } = useStore("useStore_SubtitleCountdownTimerId");
|
||||||
|
|
||||||
|
// cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる)
|
||||||
|
const scheduleCues = (offset) => {
|
||||||
|
// 字幕開始時の処理
|
||||||
|
const startFunction = (cue) => {
|
||||||
|
let send_text = "";
|
||||||
|
if (cue.actor !== "") {
|
||||||
|
send_text = `[${cue.actor}] ${cue.text}`;
|
||||||
|
} else {
|
||||||
|
send_text = `${cue.text}`;
|
||||||
|
}
|
||||||
|
console.log(`字幕開始 (index: ${cue.index}) send_text:${send_text}`);
|
||||||
|
sendTextToOverlay(send_text);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 字幕終了時の処理
|
||||||
|
const endFunction = (cue) => {
|
||||||
|
console.log(`字幕終了 (index: ${cue.index}): ${cue.text}`);
|
||||||
|
// 必要に応じた終了処理(例:テキストクリア)を実装可能
|
||||||
|
// sendTextToOverlay("");
|
||||||
|
};
|
||||||
|
|
||||||
|
currentSubtitleCues.data.forEach((cue) => {
|
||||||
|
const startDelay = cue.startTime * 1000 + offset;
|
||||||
|
const endDelay = cue.endTime * 1000 + offset;
|
||||||
|
if (startDelay >= 0) {
|
||||||
|
const timerId = setTimeout(() => startFunction(cue), startDelay);
|
||||||
|
addSubtitleTimers(timerId);
|
||||||
|
}
|
||||||
|
if (endDelay >= 0) {
|
||||||
|
const timerId = setTimeout(() => endFunction(cue), endDelay);
|
||||||
|
addSubtitleTimers(timerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// カウントダウンタイマーの開始/再登録(指定した値から1秒ごとに減らす)
|
||||||
|
const startCountdownInterval = (startValue) => {
|
||||||
|
// 既存のタイマーがあればクリア
|
||||||
|
if (currentSubtitleCountdownTimerId.data) {
|
||||||
|
clearInterval(currentSubtitleCountdownTimerId.data);
|
||||||
|
}
|
||||||
|
// 新たな開始値を設定
|
||||||
|
updateEffectiveCountdown(startValue);
|
||||||
|
const countdown_timer_id = setInterval(() => {
|
||||||
|
updateEffectiveCountdown((prev) => {
|
||||||
|
if (prev.data <= 1) {
|
||||||
|
clearInterval(currentSubtitleCountdownTimerId.data);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev.data - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
updateSubtitleCountdownTimerId(countdown_timer_id);
|
||||||
|
addSubtitleTimers(currentSubtitleCountdownTimerId.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 字幕一覧の表示(relative モードの場合、クリックでジャンプ)
|
||||||
|
// テーブル内の字幕行をクリック(relative モードのみ)でジャンプ
|
||||||
|
const handleJump = (jumpCue) => {
|
||||||
|
if (currentSubtitlePlaybackMode.data !== "relative") return;
|
||||||
|
handleSubtitlesStop();
|
||||||
|
const offset = -jumpCue.startTime * 1000;
|
||||||
|
scheduleCues(offset);
|
||||||
|
updateIsSubtitlePlaying(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 「再生開始」ボタン押下時の処理
|
||||||
|
const handleSubtitlesStart = () => {
|
||||||
|
handleSubtitlesStop();
|
||||||
|
updateIsSubtitlePlaying(true);
|
||||||
|
updateIsCuesScheduled(false);
|
||||||
|
const target_time = currentSubtitleAbsoluteTargetTime.data;
|
||||||
|
|
||||||
|
let computedCountdown = 0;
|
||||||
|
if (currentSubtitlePlaybackMode.data === "absolute") {
|
||||||
|
const now = new Date();
|
||||||
|
const hour = parseInt(target_time.hour, 10);
|
||||||
|
const minute = parseInt(target_time.minute, 10);
|
||||||
|
let targetDate = new Date(
|
||||||
|
now.getFullYear(),
|
||||||
|
now.getMonth(),
|
||||||
|
now.getDate(),
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (targetDate.getTime() < now.getTime()) {
|
||||||
|
targetDate.setDate(targetDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
computedCountdown = Math.ceil((targetDate.getTime() - now.getTime()) / 1000);
|
||||||
|
} else {
|
||||||
|
computedCountdown = 10; // relative モードの場合は固定値
|
||||||
|
}
|
||||||
|
// setInitialCountdown(computedCountdown);
|
||||||
|
// 調整値を反映した開始値
|
||||||
|
const startValue = computedCountdown + currentCountdownAdjustment.data;
|
||||||
|
startCountdownInterval(startValue);
|
||||||
|
sendTextToOverlay(startValue.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// すべてのタイマーを停止し、各状態を初期化する
|
||||||
|
const handleSubtitlesStop = () => {
|
||||||
|
currentSubtitleTimers.data.forEach((timerId) => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
clearInterval(timerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSubtitleTimers([]);
|
||||||
|
if (currentSubtitleCountdownTimerId.data) {
|
||||||
|
clearInterval(currentSubtitleCountdownTimerId.data);
|
||||||
|
updateSubtitleCountdownTimerId(null);
|
||||||
|
}
|
||||||
|
console.log("再生を停止しました。");
|
||||||
|
updateIsSubtitlePlaying(false);
|
||||||
|
// setInitialCountdown(null);
|
||||||
|
updateEffectiveCountdown(null);
|
||||||
|
updateCountdownAdjustment(0);
|
||||||
|
updateIsCuesScheduled(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentSubtitleFileName,
|
||||||
|
updateSubtitleFileName,
|
||||||
|
|
||||||
|
currentIsSubtitlePlaying,
|
||||||
|
updateIsSubtitlePlaying,
|
||||||
|
|
||||||
|
currentSubtitlePlaybackMode,
|
||||||
|
updateSubtitlePlaybackMode,
|
||||||
|
|
||||||
|
currentSubtitleAbsoluteTargetTime,
|
||||||
|
updateSubtitleAbsoluteTargetTime,
|
||||||
|
|
||||||
|
currentIsCuesScheduled,
|
||||||
|
updateIsCuesScheduled,
|
||||||
|
|
||||||
|
currentCountdownAdjustment,
|
||||||
|
updateCountdownAdjustment,
|
||||||
|
|
||||||
|
currentEffectiveCountdown,
|
||||||
|
updateEffectiveCountdown,
|
||||||
|
|
||||||
|
currentSubtitleCues,
|
||||||
|
updateSubtitleCues,
|
||||||
|
|
||||||
|
handleSubtitlesStart,
|
||||||
|
handleSubtitlesStop,
|
||||||
|
startCountdownInterval,
|
||||||
|
scheduleCues,
|
||||||
|
handleJump,
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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")
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<span>カウントダウン: {secToDayTime(currentEffectiveCountdown.data)}</span>
|
||||||
|
<div className={styles.adjust_button_container}>
|
||||||
|
{/* 1分単位の調整ボタン */}
|
||||||
|
<div className={styles.adjust_button_wrapper}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = currentEffectiveCountdown.data + 60;
|
||||||
|
updateCountdownAdjustment((prev) => prev.data + 60);
|
||||||
|
startCountdownInterval(newValue);
|
||||||
|
}}
|
||||||
|
className={styles.adjust_button}
|
||||||
|
>
|
||||||
|
▲ 1分
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = currentEffectiveCountdown.data - 60;
|
||||||
|
updateCountdownAdjustment((prev) => prev.data - 60);
|
||||||
|
startCountdownInterval(newValue);
|
||||||
|
}}
|
||||||
|
className={styles.adjust_button}
|
||||||
|
>
|
||||||
|
▼ 1分
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.adjust_button_border}></div>
|
||||||
|
{/* 1秒単位の調整ボタン */}
|
||||||
|
<div className={styles.adjust_button_wrapper}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = currentEffectiveCountdown.data + 1;
|
||||||
|
updateCountdownAdjustment((prev) => prev.data + 1);
|
||||||
|
startCountdownInterval(newValue);
|
||||||
|
}}
|
||||||
|
className={styles.adjust_button}
|
||||||
|
>
|
||||||
|
▲ 1秒
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = currentEffectiveCountdown.data - 1;
|
||||||
|
updateCountdownAdjustment((prev) => prev.data - 1);
|
||||||
|
startCountdownInterval(newValue);
|
||||||
|
}}
|
||||||
|
className={styles.adjust_button}
|
||||||
|
>
|
||||||
|
▼ 1秒
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.input_file_wrapper}>
|
||||||
|
<label htmlFor="subtitle_file_input" className={styles.input_file_label}>SRT/ASSファイルを選択</label>
|
||||||
|
<input
|
||||||
|
id="subtitle_file_input"
|
||||||
|
type="file"
|
||||||
|
accept=".srt,.ass"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className={styles.input_file_i}
|
||||||
|
/>
|
||||||
|
<p className={styles.file_name}>{currentSubtitleFileName.data}</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={handleClearFile} className={styles.file_clear}>
|
||||||
|
ファイルクリア
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.mode_selector_wrapper}>
|
||||||
|
<select
|
||||||
|
value={currentSubtitlePlaybackMode.data}
|
||||||
|
onChange={(e) => updateSubtitlePlaybackMode(e.target.value)}
|
||||||
|
className={styles.mode_selector}
|
||||||
|
>
|
||||||
|
<option className={styles.mode_selector_item} value="relative">相対モード(ボタン押下から)</option>
|
||||||
|
<option className={styles.mode_selector_item} value="absolute">絶対モード(指定時刻から)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentSubtitlePlaybackMode.data === "absolute" && (
|
||||||
|
<div className={styles.time_section}>
|
||||||
|
<label className={styles.absolute_time_label}>再生開始時刻 (HH:MM):</label>
|
||||||
|
<div className={styles.time_selects}>
|
||||||
|
<select
|
||||||
|
value={target_time.hour}
|
||||||
|
onChange={(e) => handleOnchangeTargetTime("hour", e.target.value)}
|
||||||
|
className={styles.time_selects_item}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => {
|
||||||
|
const hour = i.toString().padStart(2, "0");
|
||||||
|
return (
|
||||||
|
<option key={i} value={hour}>
|
||||||
|
{hour}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
<span>:</span>
|
||||||
|
<select
|
||||||
|
value={target_time.minute}
|
||||||
|
onChange={(e) => handleOnchangeTargetTime("minute", e.target.value)}
|
||||||
|
className={styles.time_selects_item}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 60 }, (_, i) => {
|
||||||
|
const minute = i.toString().padStart(2, "0");
|
||||||
|
return (
|
||||||
|
<option key={i} value={minute}>
|
||||||
|
{minute}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<button
|
||||||
|
onClick={handleSubtitlesStart}
|
||||||
|
className={playback_button_classname}
|
||||||
|
>
|
||||||
|
{is_playing ? "再生中" : "字幕を登録・再生"}
|
||||||
|
</button>
|
||||||
|
{is_playing &&
|
||||||
|
<button onClick={handleSubtitlesStop} className={styles.playback_stop_button}>
|
||||||
|
停止
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className={styles.subtitleSection}>
|
||||||
|
<h2>字幕一覧</h2>
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>番号</th>
|
||||||
|
<th>開始</th>
|
||||||
|
<th>終了</th>
|
||||||
|
<th>Actor</th>
|
||||||
|
<th>テキスト</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className={styles.subtitle_lists}>
|
||||||
|
{currentSubtitleCues.data.map((cue) => (
|
||||||
|
<tr
|
||||||
|
key={cue.index}
|
||||||
|
onClick={() => handleJump(cue)}
|
||||||
|
className={styles.tableRow}
|
||||||
|
>
|
||||||
|
<td>{cue.index}</td>
|
||||||
|
<td>{formatTime(cue.startTime)}</td>
|
||||||
|
<td>{formatTime(cue.endTime)}</td>
|
||||||
|
<td>{cue.actor}</td>
|
||||||
|
<td>{cue.text}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p className={styles.note}>
|
||||||
|
※ 行をクリックすると、その字幕の位置にジャンプします。(相対モードのみ)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
src-ui/plugins/plugins_index.js
Normal file
3
src-ui/plugins/plugins_index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const dev_plugins = [
|
||||||
|
{ entry_path: "dev_plugin_subtitles" }
|
||||||
|
];
|
||||||
@@ -20,6 +20,9 @@ export const store = {
|
|||||||
log_box_ref: null,
|
log_box_ref: null,
|
||||||
text_area_ref: null,
|
text_area_ref: null,
|
||||||
is_applied_init_message_box_height: false,
|
is_applied_init_message_box_height: false,
|
||||||
|
is_initialized_load_plugin: false,
|
||||||
|
is_fetched_plugins_info_already: false,
|
||||||
|
is_initialized_fetched_plugin_info: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const generatePropertyNames = (base_name) => ({
|
const generatePropertyNames = (base_name) => ({
|
||||||
@@ -34,7 +37,7 @@ const generatePropertyNames = (base_name) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const createAtomWithHook = (initialValue, base_name, options) => {
|
export const createAtomWithHook = (initialValue, base_name, options) => {
|
||||||
const property_names = generatePropertyNames(base_name);
|
const property_names = generatePropertyNames(base_name);
|
||||||
const atomInstance = atom({
|
const atomInstance = atom({
|
||||||
state: (options?.is_state_ok) ? "ok" : "pending",
|
state: (options?.is_state_ok) ? "ok" : "pending",
|
||||||
@@ -114,7 +117,10 @@ export const { atomInstance: Atom_MainFunctionsStateMemory, useHook: useStore_Ma
|
|||||||
transcription_receive: false,
|
transcription_receive: false,
|
||||||
}, "MainFunctionsStateMemory");
|
}, "MainFunctionsStateMemory");
|
||||||
export const { atomInstance: Atom_OpenedQuickSetting, useHook: useStore_OpenedQuickSetting } = createAtomWithHook("", "OpenedQuickSetting");
|
export const { atomInstance: Atom_OpenedQuickSetting, useHook: useStore_OpenedQuickSetting } = createAtomWithHook("", "OpenedQuickSetting");
|
||||||
export const { atomInstance: Atom_IsSoftwareUpdateAvailable, useHook: useStore_IsSoftwareUpdateAvailable } = createAtomWithHook(false, "IsSoftwareUpdateAvailable");
|
export const { atomInstance: Atom_LatestSoftwareVersionInfo, useHook: useStore_LatestSoftwareVersionInfo } = createAtomWithHook({
|
||||||
|
is_update_available: false,
|
||||||
|
new_version: "0.0.0",
|
||||||
|
}, "LatestSoftwareVersionInfo");
|
||||||
export const { atomInstance: Atom_InitProgress, useHook: useStore_InitProgress } = createAtomWithHook(0, "InitProgress");
|
export const { atomInstance: Atom_InitProgress, useHook: useStore_InitProgress } = createAtomWithHook(0, "InitProgress");
|
||||||
export const { atomInstance: Atom_IsBreakPoint, useHook: useStore_IsBreakPoint } = createAtomWithHook(false, "IsBreakPoint");
|
export const { atomInstance: Atom_IsBreakPoint, useHook: useStore_IsBreakPoint } = createAtomWithHook(false, "IsBreakPoint");
|
||||||
export const { atomInstance: Atom_IsSoftwareUpdating, useHook: useStore_IsSoftwareUpdating } = createAtomWithHook(false, "IsSoftwareUpdating");
|
export const { atomInstance: Atom_IsSoftwareUpdating, useHook: useStore_IsSoftwareUpdating } = createAtomWithHook(false, "IsSoftwareUpdating");
|
||||||
@@ -273,6 +279,12 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA
|
|||||||
toggle_transcription_receive: null,
|
toggle_transcription_receive: null,
|
||||||
}, "Hotkeys");
|
}, "Hotkeys");
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
export const { atomInstance: Atom_FetchedPluginsInfo, useHook: useStore_FetchedPluginsInfo } = createAtomWithHook([], "FetchedPluginsInfo");
|
||||||
|
export const { atomInstance: Atom_LoadedPlugins, useHook: useStore_LoadedPlugins } = createAtomWithHook([], "LoadedPlugins");
|
||||||
|
export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([], "SavedPluginsStatus");
|
||||||
|
export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } = createAtomWithHook([], "PluginsData");
|
||||||
|
|
||||||
// Advanced Settings
|
// Advanced Settings
|
||||||
export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress");
|
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");
|
export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createAtomWithHook("9000", "OscPort");
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ export const ui_configs = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// true: src-ui\plugins false: src-tauri\target\debug\plugins
|
||||||
|
export const IS_PLUGIN_PATH_DEV_MODE = false;
|
||||||
|
|
||||||
|
// true: dev_vrct_plugins_list.json false: vrct_plugins_list.json
|
||||||
|
export const IS_PLUGIN_LIST_URL_DEV_MODE = false;
|
||||||
|
|
||||||
|
export const getPluginsList = () => {
|
||||||
|
const base_url = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/";
|
||||||
|
const plugins_list_url = (IS_PLUGIN_LIST_URL_DEV_MODE)
|
||||||
|
? base_url + "dev_vrct_plugins_list.json"
|
||||||
|
: base_url + "vrct_plugins_list.json";
|
||||||
|
return plugins_list_url;
|
||||||
|
};
|
||||||
|
if (IS_PLUGIN_PATH_DEV_MODE || IS_PLUGIN_LIST_URL_DEV_MODE) console.warn("ui_configs IS_PLUGIN_PATH_DEV_MODE or IS_PLUGIN_LIST_URL_DEV_MODE is true. Turn to 'false' when it's production environment.");
|
||||||
|
|
||||||
export const translator_status = [
|
export const translator_status = [
|
||||||
{ id: "DeepL", label: "DeepL", is_available: false },
|
{ id: "DeepL", label: "DeepL", is_available: false },
|
||||||
{ id: "DeepL_API", label: `DeepL API`, is_available: false },
|
{ id: "DeepL_API", label: `DeepL API`, is_available: false },
|
||||||
|
|||||||
133
vite.config.js
133
vite.config.js
@@ -1,62 +1,103 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
|
import yaml from "@rollup/plugin-yaml";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
import { dev_plugins } from "./src-ui/plugins/plugins_index.js";
|
||||||
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(async () => ({
|
export default defineConfig(async () => {
|
||||||
plugins: [react(), svgr()],
|
const plugin_aliases = await getPluginAliases();
|
||||||
assetsInclude: ["**/*.yml"],
|
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
return {
|
||||||
//
|
plugins: [
|
||||||
// 1. prevent vite from obscuring rust errors
|
yaml({ include: ["**/*.yml", "**/*.yaml"] }),
|
||||||
clearScreen: false,
|
react(),
|
||||||
// 2. tauri expects a fixed port, fail if that port is not available
|
svgr(),
|
||||||
server: {
|
],
|
||||||
port: 1420,
|
|
||||||
strictPort: true,
|
|
||||||
watch: {
|
|
||||||
// 3. tell vite to ignore watching `src-tauri`
|
|
||||||
ignored: ["**/src-tauri/**"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
outDir: path.resolve(__dirname, "dist"),
|
//
|
||||||
rollupOptions: {
|
// 1. prevent vite from obscuring rust errors
|
||||||
input: {
|
clearScreen: false,
|
||||||
main: path.resolve(__dirname, "index.html"),
|
// 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: {
|
build: {
|
||||||
alias: {
|
outDir: path.resolve(__dirname, "dist"),
|
||||||
"@root": path.resolve(__dirname),
|
rollupOptions: {
|
||||||
"@test_data": path.resolve(__dirname, "./test_data.js"),
|
input: {
|
||||||
|
main: path.resolve(__dirname, "index.html"),
|
||||||
"@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"),
|
sourcemap: true,
|
||||||
"@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"),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
css: {
|
resolve: {
|
||||||
preprocessorOptions: {
|
alias: {
|
||||||
scss: {
|
"@root": path.resolve(__dirname),
|
||||||
api: "modern-compiler"
|
"@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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user