Merge branch 'plugins_system' into develop
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,4 +43,5 @@ dist-ssr
|
||||
.venv
|
||||
|
||||
# Customize
|
||||
/build
|
||||
/build
|
||||
error.txt
|
||||
@@ -243,4 +243,31 @@ config_page:
|
||||
open_config_filepath:
|
||||
label: "Open Config File"
|
||||
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:
|
||||
label: "設定ファイルを開く"
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/standalone": "7.26.9",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@mui/material": "6.2.0",
|
||||
@@ -34,17 +35,21 @@
|
||||
"i18next": "24.1.0",
|
||||
"jotai": "2.10.3",
|
||||
"js-base64": "3.7.7",
|
||||
"js-yaml": "4.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "5.0.0",
|
||||
"react-i18next": "15.2.0",
|
||||
"react-resizable-layout": "0.7.2"
|
||||
"react-resizable-layout": "0.7.2",
|
||||
"semver": "7.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@tauri-apps/cli": "1.6.3",
|
||||
"npm-run-all": "4.1.5",
|
||||
"sass": "1.79.4",
|
||||
"vite": "6.0.3",
|
||||
"source-map": "0.7.4",
|
||||
"vite": "6.2.5",
|
||||
"vite-plugin-svgr": "4.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +555,18 @@ class Config:
|
||||
self._HOTKEYS[key] = value
|
||||
self.saveConfig(inspect.currentframe().f_code.co_name, self.HOTKEYS, immediate_save=True)
|
||||
|
||||
@property
|
||||
@json_serializable('PLUGINS_STATUS')
|
||||
def PLUGINS_STATUS(self):
|
||||
return self._PLUGINS_STATUS
|
||||
|
||||
@PLUGINS_STATUS.setter
|
||||
def PLUGINS_STATUS(self, value):
|
||||
if isinstance(value, list):
|
||||
if all(isinstance(item, dict) for item in value):
|
||||
self._PLUGINS_STATUS = value
|
||||
self.saveConfig(inspect.currentframe().f_code.co_name, self.PLUGINS_STATUS, immediate_save=True)
|
||||
|
||||
@property
|
||||
@json_serializable('MIC_AVG_LOGPROB')
|
||||
def MIC_AVG_LOGPROB(self):
|
||||
@@ -1068,6 +1080,7 @@ class Config:
|
||||
"toggle_transcription_send": None,
|
||||
"toggle_transcription_receive": None,
|
||||
}
|
||||
self._PLUGINS_STATUS = []
|
||||
self._MIC_AVG_LOGPROB = -0.8
|
||||
self._MIC_NO_SPEECH_PROB = 0.6
|
||||
self._AUTO_SPEAKER_SELECT = True
|
||||
|
||||
@@ -447,11 +447,11 @@ class Controller:
|
||||
return {"status":200, "result":config.VERSION}
|
||||
|
||||
def checkSoftwareUpdated(self) -> dict:
|
||||
update_flag = model.checkSoftwareUpdated()
|
||||
software_update_info = model.checkSoftwareUpdated()
|
||||
self.run(
|
||||
200,
|
||||
self.run_mapping["update_software_flag"],
|
||||
update_flag,
|
||||
self.run_mapping["software_update_info"],
|
||||
software_update_info,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -1061,6 +1061,15 @@ class Controller:
|
||||
config.HOTKEYS = data
|
||||
return {"status":200, "result":config.HOTKEYS}
|
||||
|
||||
@staticmethod
|
||||
def getPluginsStatus(*args, **kwargs) -> dict:
|
||||
return {"status":200, "result":config.PLUGINS_STATUS}
|
||||
|
||||
@staticmethod
|
||||
def setPluginsStatus(data, *args, **kwargs) -> dict:
|
||||
config.PLUGINS_STATUS = data
|
||||
return {"status":200, "result":config.PLUGINS_STATUS}
|
||||
|
||||
@staticmethod
|
||||
def getSpeakerAvgLogprob(*args, **kwargs) -> dict:
|
||||
return {"status":200, "result":config.SPEAKER_AVG_LOGPROB}
|
||||
|
||||
@@ -38,7 +38,7 @@ run_mapping = {
|
||||
"mic_device_list":"/run/mic_device_list",
|
||||
"speaker_device_list":"/run/speaker_device_list",
|
||||
|
||||
"update_software_flag":"/run/update_software_flag",
|
||||
"software_update_info":"/run/software_update_info",
|
||||
|
||||
"initialization_progress":"/run/initialization_progress",
|
||||
"initialization_complete":"/run/initialization_complete",
|
||||
@@ -190,6 +190,9 @@ mapping = {
|
||||
"/get/data/hotkeys": {"status": True, "variable":controller.getHotkeys},
|
||||
"/set/data/hotkeys": {"status": True, "variable":controller.setHotkeys},
|
||||
|
||||
"/get/data/plugins_status": {"status": True, "variable":controller.getPluginsStatus},
|
||||
"/set/data/plugins_status": {"status": True, "variable":controller.setPluginsStatus},
|
||||
|
||||
"/get/data/mic_avg_logprob": {"status": True, "variable":controller.getMicAvgLogprob},
|
||||
"/set/data/mic_avg_logprob": {"status": True, "variable":controller.setMicAvgLogprob},
|
||||
|
||||
|
||||
@@ -320,6 +320,7 @@ class Model:
|
||||
def checkSoftwareUpdated():
|
||||
# check update
|
||||
update_flag = False
|
||||
version = ""
|
||||
try:
|
||||
response = requests_get(config.GITHUB_URL)
|
||||
json_data = response.json()
|
||||
@@ -331,7 +332,10 @@ class Model:
|
||||
update_flag = True
|
||||
except Exception:
|
||||
errorLogging()
|
||||
return update_flag
|
||||
return {
|
||||
"is_update_available": update_flag,
|
||||
"new_version": version,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def updateSoftware():
|
||||
|
||||
507
src-tauri/Cargo.lock
generated
507
src-tauri/Cargo.lock
generated
@@ -6,7 +6,9 @@ version = 3
|
||||
name = "VRCT"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"font-kit",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -218,6 +220,9 @@ name = "bytes"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
@@ -729,7 +734,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.8.19",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -971,6 +976,12 @@ dependencies = [
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
@@ -984,8 +995,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -1297,6 +1311,25 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap 2.7.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1361,12 +1394,86 @@ dependencies = [
|
||||
"itoa 1.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.14",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
@@ -1613,6 +1720,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@@ -1849,6 +1962,12 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.2"
|
||||
@@ -1859,6 +1978,34 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.6.0"
|
||||
@@ -1998,6 +2145,50 @@ dependencies = [
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -2516,6 +2707,67 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
@@ -2544,6 +2796,37 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.19"
|
||||
@@ -2565,6 +2848,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -2577,6 +2869,39 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.22.0"
|
||||
@@ -2659,6 +2984,18 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa 1.0.14",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.12.0"
|
||||
@@ -2784,6 +3121,16 @@ version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soup2"
|
||||
version = "0.2.1"
|
||||
@@ -2812,6 +3159,12 @@ dependencies = [
|
||||
"system-deps 5.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -2881,6 +3234,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.1"
|
||||
@@ -2892,6 +3251,27 @@ dependencies = [
|
||||
"syn 2.0.94",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "5.0.0"
|
||||
@@ -3000,6 +3380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf327e247698d3f39af8aa99401c9708384290d1f5c544bf5d251d44c2fea22"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cocoa 0.24.1",
|
||||
"dirs-next",
|
||||
"dunce",
|
||||
@@ -3014,6 +3395,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"ignore",
|
||||
"indexmap 1.9.3",
|
||||
"log",
|
||||
"objc",
|
||||
"once_cell",
|
||||
@@ -3024,6 +3406,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3296,7 +3679,44 @@ checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3367,6 +3787,12 @@ dependencies = [
|
||||
"winnow 0.6.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
@@ -3428,6 +3854,12 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@@ -3446,6 +3878,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
@@ -3491,6 +3929,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.0.11"
|
||||
@@ -3539,6 +3983,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||
dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
@@ -3576,6 +4029,19 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.99"
|
||||
@@ -3605,6 +4071,29 @@ version = "0.2.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webkit2gtk"
|
||||
version = "0.18.2"
|
||||
@@ -3652,6 +4141,12 @@ dependencies = [
|
||||
"system-deps 6.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.19.1"
|
||||
@@ -4058,6 +4553,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -11,11 +11,13 @@ edition = "2021"
|
||||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1", features = [ "window-hide", "window-set-focus", "global-shortcut-all", "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] }
|
||||
tauri = { version = "1", features = [ "fs-remove-dir", "fs-read-file", "fs-create-dir", "fs-write-file", "fs-exists", "http-request", "fs-read-dir", "window-hide", "window-set-focus", "global-shortcut-all", "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
font-kit = "0.14.2"
|
||||
window-shadows = { git = "https://github.com/tauri-apps/window-shadows.git" }
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
base64 = "0.22.1"
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
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();
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![get_font_list])
|
||||
.invoke_handler(tauri::generate_handler![get_font_list, download_zip_asset])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -45,4 +45,23 @@ async fn get_font_list() -> Vec<String> {
|
||||
}
|
||||
|
||||
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": {
|
||||
"all": true
|
||||
},
|
||||
"fs": {
|
||||
"readDir": true,
|
||||
"readFile": true,
|
||||
"exists": true,
|
||||
"writeFile": true,
|
||||
"createDir": true,
|
||||
"removeDir": true,
|
||||
"scope": ["$RESOURCE/**", "**/src-tauri/target/debug/plugins/**"]
|
||||
},
|
||||
"http": {
|
||||
"request": true,
|
||||
"scope": [
|
||||
"https://api.github.com/repos/**",
|
||||
"https://github.com/**",
|
||||
"https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json",
|
||||
"https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/dev_vrct_plugins_list.json"
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true,
|
||||
@@ -68,7 +86,8 @@
|
||||
"bin/VRCT-sidecar"
|
||||
],
|
||||
"resources": {
|
||||
"bin/_internal": "_internal"
|
||||
"bin/_internal": "_internal",
|
||||
"plugins": "plugins"
|
||||
},
|
||||
"windows": {
|
||||
"nsis": {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UiSizeController,
|
||||
FontFamilyController,
|
||||
TransparencyController,
|
||||
PluginsController,
|
||||
} from "./_app_controllers/index.js";
|
||||
|
||||
import { WindowTitleBar } from "./window_title_bar/WindowTitleBar";
|
||||
@@ -20,6 +21,7 @@ import { ModalController } from "./modal_controller/ModalController";
|
||||
import { SnackbarController } from "./snackbar_controller/SnackbarController";
|
||||
import styles from "./App.module.scss";
|
||||
import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common";
|
||||
import { AppErrorBoundary } from "./error_boundary/AppErrorBoundary";
|
||||
|
||||
export const App = () => {
|
||||
const { currentIsVrctAvailable } = useIsVrctAvailable();
|
||||
@@ -29,22 +31,24 @@ export const App = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<KeyEventController />
|
||||
<StartPythonController />
|
||||
<GlobalHotKeyController />
|
||||
<UiLanguageController />
|
||||
<ConfigPageCloseTriggerController />
|
||||
<UiSizeController />
|
||||
<FontFamilyController />
|
||||
<TransparencyController />
|
||||
<WindowGeometryController />
|
||||
<AppErrorBoundary >
|
||||
<KeyEventController />
|
||||
<StartPythonController />
|
||||
<GlobalHotKeyController />
|
||||
<UiLanguageController />
|
||||
<ConfigPageCloseTriggerController />
|
||||
<UiSizeController />
|
||||
<FontFamilyController />
|
||||
<TransparencyController />
|
||||
<WindowGeometryController />
|
||||
|
||||
{(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false)
|
||||
? <SplashComponent />
|
||||
: <Contents key={i18n.language}/>
|
||||
}
|
||||
{(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false)
|
||||
? <SplashComponent />
|
||||
: <Contents key={i18n.language} />
|
||||
}
|
||||
|
||||
<SnackbarController />
|
||||
<SnackbarController />
|
||||
</AppErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -53,6 +57,8 @@ const Contents = () => {
|
||||
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
|
||||
return (
|
||||
<>
|
||||
<PluginsController />
|
||||
|
||||
<WindowTitleBar />
|
||||
{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 { UiSizeController } from "./UiSizeController";
|
||||
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,
|
||||
Vr,
|
||||
Hotkeys,
|
||||
// Plugins,
|
||||
Supporters,
|
||||
AboutVrct,
|
||||
} from "@setting_box";
|
||||
@@ -32,6 +33,8 @@ export const SettingBox = () => {
|
||||
return <Hotkeys />;
|
||||
case "advanced_settings":
|
||||
return <AdvancedSettings />;
|
||||
// case "plugins":
|
||||
// return <Plugins />;
|
||||
case "supporters":
|
||||
return <Supporters />;
|
||||
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 {
|
||||
RadioButton,
|
||||
} from "../index";
|
||||
import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton";
|
||||
|
||||
export const DownloadModels = (props) => {
|
||||
const options = props.options.map(item => ({
|
||||
@@ -19,47 +17,9 @@ export const DownloadModels = (props) => {
|
||||
options={options}
|
||||
checked_variable={props.checked_variable}
|
||||
column={true}
|
||||
ChildComponent={ModelSelector}
|
||||
ChildComponent={_DownloadButton}
|
||||
downloadStartFunction={props.downloadStartFunction}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ModelSelector = ({option, ...props}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderContent = () => {
|
||||
const circular_progress = Math.floor(option.progress / 10) * 10;
|
||||
|
||||
switch (true) {
|
||||
case option.progress !== null:
|
||||
return (
|
||||
<>
|
||||
<CircularProgress
|
||||
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
|
||||
value={circular_progress}
|
||||
size="3rem"
|
||||
sx={{ color: "var(--primary_300_color)" }}
|
||||
/>
|
||||
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
|
||||
</>
|
||||
);
|
||||
case option.is_pending:
|
||||
return <CircularProgress size="3rem" sx={{ color: "var(--dark_600_color)" }}/>;
|
||||
case !option.is_downloaded:
|
||||
return (
|
||||
<button
|
||||
className={styles.download_button}
|
||||
onClick={() => props.downloadStartFunction(option.id)}
|
||||
>
|
||||
<p className={styles.download_button_label}>{t("config_page.model_download_button_label")}</p>
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={styles.download_container}>{renderContent()}</div>;
|
||||
};
|
||||
};
|
||||
@@ -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 { useState, useRef, useEffect } from "react";
|
||||
import DeleteSvg from "@images/cancel.svg?react";
|
||||
import { clsx } from "clsx";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const HotkeysEntry = (props) => {
|
||||
const [isAcceptingInput, setIsAcceptingInput] = useState(false);
|
||||
|
||||
@@ -12,4 +12,5 @@ export { Slider } from "./slider/Slider";
|
||||
export { SwitchBox } from "./switch_box/SwitchBox";
|
||||
export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
|
||||
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
|
||||
export { DownloadModels } from "./download_models/DownloadModels";
|
||||
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 styles from "./Slider.module.scss";
|
||||
import MUI_Slider from "@mui/material/Slider";
|
||||
import { clsx } from "clsx";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const Slider = (props) => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "@scss_mixins";
|
||||
|
||||
.switchbox_container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
@@ -22,6 +21,7 @@
|
||||
}
|
||||
|
||||
.toggle_control {
|
||||
position: relative;
|
||||
@include toggle_control_styles;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "./AdvancedSettings.module.scss";
|
||||
|
||||
import { Plugins } from "./plugins/Plugins";
|
||||
|
||||
import { useOpenFolder } from "@logics_common";
|
||||
import {
|
||||
useOscIpAddress,
|
||||
@@ -10,22 +12,35 @@ import {
|
||||
|
||||
import {
|
||||
ActionButtonContainer,
|
||||
EntryContainer,
|
||||
EntryWithSaveButtonContainer,
|
||||
} from "../_templates/Templates";
|
||||
|
||||
import {
|
||||
SectionLabelComponent,
|
||||
} from "../_components/";
|
||||
|
||||
|
||||
import OpenFolderSvg from "@images/open_folder.svg?react";
|
||||
import HelpSvg from "@images/help.svg?react";
|
||||
|
||||
export const AdvancedSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OscIpAddressContainer />
|
||||
<OscPortContainer />
|
||||
<OpenConfigFolderContainer />
|
||||
<OpenSwitchComputeDeviceModalContainer />
|
||||
</>
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
|
||||
<OscIpAddressContainer />
|
||||
<OscPortContainer />
|
||||
<OpenConfigFolderContainer />
|
||||
<OpenSwitchComputeDeviceModalContainer />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionLabelComponent label={t("config_page.advanced_settings.section_label_plugins")} />
|
||||
<Plugins />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
&.flex_column {
|
||||
flex-direction: column;
|
||||
}
|
||||
border-bottom: solid 0.1rem var(--dark_800_color);
|
||||
}
|
||||
|
||||
.switch_section_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
gap: 6.4rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -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 { Vr } from "./vr/Vr";
|
||||
export { Hotkeys } from "./hotkeys/Hotkeys";
|
||||
// export { Plugins } from "./plugins/Plugins";
|
||||
export { AboutVrct } from "./about_vrct/AboutVrct";
|
||||
export { Supporters } from "./supporters/Supporters";
|
||||
@@ -3,7 +3,7 @@ import fanbox_logo from "@images/supporters/fanbox_logo.png";
|
||||
import kofi_logo from "@images/supporters/kofi_logo.png";
|
||||
import patreon_logo from "@images/supporters/patreon_logo.png";
|
||||
import styles from "./SupportUsContainer.module.scss";
|
||||
import { clsx } from "clsx";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const SupportUsContainer = () => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { clsx } from "clsx";
|
||||
import clsx from "clsx";
|
||||
import styles from "./Vr.module.scss";
|
||||
import { ui_configs } from "@ui_configs";
|
||||
import { Slider } from "../_components/";
|
||||
|
||||
@@ -12,6 +12,7 @@ export const SidebarSection = () => {
|
||||
<Tab tab_id="vr" />
|
||||
<Tab tab_id="others" />
|
||||
<Tab tab_id="hotkeys" />
|
||||
{/* <Tab tab_id="plugins" /> */}
|
||||
<Tab tab_id="advanced_settings" />
|
||||
</div>
|
||||
<div className={styles.separated_tabs_wrapper}>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { clsx } from "clsx";
|
||||
import clsx from "clsx";
|
||||
import styles from "./VersionLabel.module.scss";
|
||||
|
||||
import { useSoftwareVersion } from "@logics_configs";
|
||||
import { useComputeMode } from "@logics_common";
|
||||
import { useSoftwareVersion, useComputeMode } from "@logics_common";
|
||||
import CopySvg from "@images/copy.svg?react";
|
||||
import CheckMarkSvg from "@images/check_mark.svg?react";
|
||||
|
||||
|
||||
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 { useEffect } from "react";
|
||||
|
||||
import { PluginHost } from "./PluginHost";
|
||||
|
||||
import { usePlugins } from "@logics_configs";
|
||||
|
||||
export const MainSection = () => {
|
||||
const { currentPluginsData } = usePlugins();
|
||||
|
||||
const render_plugins = currentPluginsData.data.filter((plugin) => (
|
||||
plugin.is_downloaded &&
|
||||
plugin.is_enabled &&
|
||||
plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||
plugin.downloaded_plugin_info.location === "main_section"
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<TopBar />
|
||||
<MessageContainer />
|
||||
{render_plugins.length
|
||||
? <PluginHost render_components={render_plugins}/>
|
||||
: <MessageContainer />
|
||||
}
|
||||
<HandleLanguageSelector />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
// justify-content: space-between;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.language_selector_container {
|
||||
|
||||
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 { useStore_OpenedQuickSetting } from "@store";
|
||||
import { useIsSoftwareUpdateAvailable } from "@logics_common";
|
||||
import { useSoftwareVersion } from "@logics_common";
|
||||
import { useIsEnabledOverlaySmallLog, useIsEnabledOverlayLargeLog, useEnableVrcMicMuteSync } from "@logics_configs";
|
||||
import { OpenQuickSettingButton } from "./_buttons/OpenQuickSettingButton";
|
||||
|
||||
@@ -66,9 +66,9 @@ const OpenVrcMicMuteSyncQuickSetting = () => {
|
||||
};
|
||||
|
||||
const SoftwareUpdateAvailableButton = () => {
|
||||
const { currentIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable();
|
||||
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||
const { t } = useTranslation();
|
||||
if (currentIsSoftwareUpdateAvailable.data === false) return null;
|
||||
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
|
||||
|
||||
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import clsx from "clsx";
|
||||
import styles from "./UpdateModal.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useStore_OpenedQuickSetting } from "@store";
|
||||
import { useComputeMode, useUpdateSoftware } from "@logics_common";
|
||||
import { useIsSoftwareUpdating, useIsSoftwareUpdateAvailable } from "@logics_common";
|
||||
import clsx from "clsx";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
import {
|
||||
useComputeMode,
|
||||
useUpdateSoftware,
|
||||
useIsSoftwareUpdating,
|
||||
useSoftwareVersion,
|
||||
} from "@logics_common";
|
||||
|
||||
import { PluginCompatibilityList } from "./plugins_compatibility_list/PluginCompatibilityList";
|
||||
|
||||
export const UpdateModal = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -11,9 +18,10 @@ export const UpdateModal = () => {
|
||||
const { updateSoftware, updateSoftware_CUDA } = useUpdateSoftware();
|
||||
const { updateIsSoftwareUpdating } = useIsSoftwareUpdating();
|
||||
const { currentComputeMode } = useComputeMode();
|
||||
const { currentIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable();
|
||||
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||
const { isAnyPluginEnabled } = usePlugins();
|
||||
|
||||
const is_latest_version_already = currentIsSoftwareUpdateAvailable.data === false;
|
||||
const is_latest_version_already = currentLatestSoftwareVersionInfo.data.is_update_available === false;
|
||||
const is_cpu_version = currentComputeMode.data === "cpu";
|
||||
|
||||
const onClickUpdateSoftware = () => {
|
||||
@@ -37,30 +45,34 @@ export const UpdateModal = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.update_section}>
|
||||
<div className={styles.cpu_section}>
|
||||
<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 className={styles.update_section_wrapper}>
|
||||
{isAnyPluginEnabled() && <PluginCompatibilityList />}
|
||||
<div className={styles.update_section}>
|
||||
<div className={styles.cpu_section}>
|
||||
<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 className={styles.version_desc_container}>
|
||||
<VersionDescComponent desc={t("update_modal.cpu_desc")} />
|
||||
<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>
|
||||
</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 className={styles.button_wrapper}>
|
||||
<button className={styles.deny_button} onClick={() => updateOpenedQuickSetting("")} >{t("update_modal.close_modal")}</button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
justify-content: safe center;
|
||||
align-items: center;
|
||||
gap: 2.4rem;
|
||||
}
|
||||
@@ -16,6 +16,14 @@
|
||||
gap: 8rem;
|
||||
}
|
||||
|
||||
.update_section_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.update_section {
|
||||
border: 0.1rem solid var(--dark_600_color);
|
||||
border-radius: 0.4rem;
|
||||
|
||||
@@ -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 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";
|
||||
export const Checkbox = ({
|
||||
checkboxId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export { useSoftwareVersion } from "./useSoftwareVersion";
|
||||
export { useComputeMode } from "./useComputeMode";
|
||||
export { useInitProgress } from "./useInitProgress";
|
||||
export { useIsBackendReady } from "./useIsBackendReady";
|
||||
export { useWindow } from "./useWindow";
|
||||
export { useIsOpenedConfigPage } from "./useIsOpenedConfigPage";
|
||||
export { useIsSoftwareUpdateAvailable } from "./useIsSoftwareUpdateAvailable";
|
||||
export { useIsSoftwareUpdating } from "./useIsSoftwareUpdating";
|
||||
export { useNotificationStatus } from "./useNotificationStatus";
|
||||
export { useOpenFolder } from "./useOpenFolder";
|
||||
@@ -11,4 +11,5 @@ export { useMessage } from "./useMessage";
|
||||
export { useUpdateSoftware } from "./useUpdateSoftware";
|
||||
export { useVolume } from "./useVolume";
|
||||
export { useHandleNetworkConnection } from "./useHandleNetworkConnection";
|
||||
export { useIsVrctAvailable } from "./useIsVrctAvailable";
|
||||
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 { usePlugins } from "./plugins/usePlugins";
|
||||
|
||||
export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition";
|
||||
export { useSoftwareVersion } from "./useSoftwareVersion";
|
||||
export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition";
|
||||
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,
|
||||
useHandleNetworkConnection,
|
||||
|
||||
useSoftwareVersion,
|
||||
useComputeMode,
|
||||
useInitProgress,
|
||||
useIsBackendReady,
|
||||
useWindow,
|
||||
useMessage,
|
||||
useVolume,
|
||||
useIsSoftwareUpdateAvailable,
|
||||
} from "@logics_common";
|
||||
|
||||
import {
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from "@logics_main";
|
||||
|
||||
import {
|
||||
useSoftwareVersion,
|
||||
useEnableAutoMicSelect,
|
||||
useEnableAutoSpeakerSelect,
|
||||
useMicHostList,
|
||||
@@ -73,6 +72,7 @@ import {
|
||||
useOverlayShowOnlyTranslatedMessages,
|
||||
useEnableNotificationVrcSfx,
|
||||
useHotkeys,
|
||||
usePlugins,
|
||||
useOscIpAddress,
|
||||
useOscPort,
|
||||
} from "@logics_configs";
|
||||
@@ -104,7 +104,7 @@ export const useReceiveRoutes = () => {
|
||||
addSentMessageLog,
|
||||
addReceivedMessageLog,
|
||||
} = useMessage();
|
||||
const { updateIsSoftwareUpdateAvailable } = useIsSoftwareUpdateAvailable();
|
||||
const { updateLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||
const { updateSoftwareVersion } = useSoftwareVersion();
|
||||
const { updateEnableAutoMicSelect } = useEnableAutoMicSelect();
|
||||
const { updateEnableAutoSpeakerSelect } = useEnableAutoSpeakerSelect();
|
||||
@@ -176,6 +176,7 @@ export const useReceiveRoutes = () => {
|
||||
const { updateEnableNotificationVrcSfx } = useEnableNotificationVrcSfx();
|
||||
|
||||
const { updateHotkeys } = useHotkeys();
|
||||
const { updateSavedPluginsStatus } = usePlugins();
|
||||
|
||||
const { updateOscIpAddress } = useOscIpAddress();
|
||||
const { updateOscPort } = useOscPort();
|
||||
@@ -205,7 +206,12 @@ export const useReceiveRoutes = () => {
|
||||
"/set/data/main_window_geometry": () => {},
|
||||
"/run/open_filepath_logs": () => console.log("Opened Directory, Message Logs"),
|
||||
"/run/open_filepath_config_file": () => console.log("Opened Directory, Config File"),
|
||||
"/run/update_software_flag": updateIsSoftwareUpdateAvailable,
|
||||
"/run/software_update_info": (payload) => {
|
||||
updateLatestSoftwareVersionInfo(prev => ({
|
||||
is_update_available: payload.is_update_available,
|
||||
new_version: payload.new_version || prev.data.new_version,
|
||||
}));
|
||||
},
|
||||
"/run/connected_network": handleNetworkConnection,
|
||||
|
||||
// Main Page
|
||||
@@ -488,6 +494,10 @@ export const useReceiveRoutes = () => {
|
||||
"/get/data/hotkeys": updateHotkeys,
|
||||
"/set/data/hotkeys": updateHotkeys,
|
||||
|
||||
// Plugins
|
||||
"/get/data/plugins_status": updateSavedPluginsStatus,
|
||||
"/set/data/plugins_status": updateSavedPluginsStatus,
|
||||
|
||||
// Advanced Settings
|
||||
"/get/data/osc_ip_address": updateOscIpAddress,
|
||||
"/set/data/osc_ip_address": updateOscIpAddress,
|
||||
|
||||
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,
|
||||
text_area_ref: null,
|
||||
is_applied_init_message_box_height: false,
|
||||
is_initialized_load_plugin: false,
|
||||
is_fetched_plugins_info_already: false,
|
||||
is_initialized_fetched_plugin_info: false,
|
||||
};
|
||||
|
||||
const generatePropertyNames = (base_name) => ({
|
||||
@@ -34,7 +37,7 @@ const generatePropertyNames = (base_name) => ({
|
||||
});
|
||||
|
||||
|
||||
const createAtomWithHook = (initialValue, base_name, options) => {
|
||||
export const createAtomWithHook = (initialValue, base_name, options) => {
|
||||
const property_names = generatePropertyNames(base_name);
|
||||
const atomInstance = atom({
|
||||
state: (options?.is_state_ok) ? "ok" : "pending",
|
||||
@@ -114,7 +117,10 @@ export const { atomInstance: Atom_MainFunctionsStateMemory, useHook: useStore_Ma
|
||||
transcription_receive: false,
|
||||
}, "MainFunctionsStateMemory");
|
||||
export const { atomInstance: Atom_OpenedQuickSetting, useHook: useStore_OpenedQuickSetting } = createAtomWithHook("", "OpenedQuickSetting");
|
||||
export const { atomInstance: Atom_IsSoftwareUpdateAvailable, useHook: useStore_IsSoftwareUpdateAvailable } = createAtomWithHook(false, "IsSoftwareUpdateAvailable");
|
||||
export const { atomInstance: Atom_LatestSoftwareVersionInfo, useHook: useStore_LatestSoftwareVersionInfo } = createAtomWithHook({
|
||||
is_update_available: false,
|
||||
new_version: "0.0.0",
|
||||
}, "LatestSoftwareVersionInfo");
|
||||
export const { atomInstance: Atom_InitProgress, useHook: useStore_InitProgress } = createAtomWithHook(0, "InitProgress");
|
||||
export const { atomInstance: Atom_IsBreakPoint, useHook: useStore_IsBreakPoint } = createAtomWithHook(false, "IsBreakPoint");
|
||||
export const { atomInstance: Atom_IsSoftwareUpdating, useHook: useStore_IsSoftwareUpdating } = createAtomWithHook(false, "IsSoftwareUpdating");
|
||||
@@ -273,6 +279,12 @@ export const { atomInstance: Atom_Hotkeys, useHook: useStore_Hotkeys } = createA
|
||||
toggle_transcription_receive: null,
|
||||
}, "Hotkeys");
|
||||
|
||||
// Plugins
|
||||
export const { atomInstance: Atom_FetchedPluginsInfo, useHook: useStore_FetchedPluginsInfo } = createAtomWithHook([], "FetchedPluginsInfo");
|
||||
export const { atomInstance: Atom_LoadedPlugins, useHook: useStore_LoadedPlugins } = createAtomWithHook([], "LoadedPlugins");
|
||||
export const { atomInstance: Atom_SavedPluginsStatus, useHook: useStore_SavedPluginsStatus } = createAtomWithHook([], "SavedPluginsStatus");
|
||||
export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } = createAtomWithHook([], "PluginsData");
|
||||
|
||||
// Advanced Settings
|
||||
export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress");
|
||||
export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createAtomWithHook("9000", "OscPort");
|
||||
|
||||
@@ -52,6 +52,21 @@ export const ui_configs = {
|
||||
]
|
||||
};
|
||||
|
||||
// true: src-ui\plugins false: src-tauri\target\debug\plugins
|
||||
export const IS_PLUGIN_PATH_DEV_MODE = false;
|
||||
|
||||
// true: dev_vrct_plugins_list.json false: vrct_plugins_list.json
|
||||
export const IS_PLUGIN_LIST_URL_DEV_MODE = false;
|
||||
|
||||
export const getPluginsList = () => {
|
||||
const base_url = "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/";
|
||||
const plugins_list_url = (IS_PLUGIN_LIST_URL_DEV_MODE)
|
||||
? base_url + "dev_vrct_plugins_list.json"
|
||||
: base_url + "vrct_plugins_list.json";
|
||||
return plugins_list_url;
|
||||
};
|
||||
if (IS_PLUGIN_PATH_DEV_MODE || IS_PLUGIN_LIST_URL_DEV_MODE) console.warn("ui_configs IS_PLUGIN_PATH_DEV_MODE or IS_PLUGIN_LIST_URL_DEV_MODE is true. Turn to 'false' when it's production environment.");
|
||||
|
||||
export const translator_status = [
|
||||
{ id: "DeepL", label: "DeepL", is_available: false },
|
||||
{ id: "DeepL_API", label: `DeepL API`, is_available: false },
|
||||
|
||||
133
vite.config.js
133
vite.config.js
@@ -1,62 +1,103 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import yaml from "@rollup/plugin-yaml";
|
||||
import path from "path";
|
||||
|
||||
import { dev_plugins } from "./src-ui/plugins/plugins_index.js";
|
||||
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react(), svgr()],
|
||||
assetsInclude: ["**/*.yml"],
|
||||
export default defineConfig(async () => {
|
||||
const plugin_aliases = await getPluginAliases();
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
return {
|
||||
plugins: [
|
||||
yaml({ include: ["**/*.yml", "**/*.yaml"] }),
|
||||
react(),
|
||||
svgr(),
|
||||
],
|
||||
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "dist"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, "index.html"),
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@root": path.resolve(__dirname),
|
||||
"@test_data": path.resolve(__dirname, "./test_data.js"),
|
||||
|
||||
"@ui_configs": path.resolve(__dirname, "src-ui/ui_configs.js"),
|
||||
"@scss_mixins": path.resolve(__dirname, "src-ui/common_css/mixins.scss"),
|
||||
"@store": path.resolve(__dirname, "src-ui/store.js"),
|
||||
"@images": path.resolve(__dirname, "src-ui/assets"),
|
||||
"@utils": path.resolve(__dirname, "src-ui/utils.js"),
|
||||
"@logics": path.resolve(__dirname, "src-ui/logics"),
|
||||
"@logics_common": path.resolve(__dirname, "src-ui/logics/common"),
|
||||
"@logics_main": path.resolve(__dirname, "src-ui/logics/main"),
|
||||
"@logics_configs": path.resolve(__dirname, "src-ui/logics/configs"),
|
||||
|
||||
"@setting_box": path.resolve(__dirname, "src-ui/app/config_page/setting_section/setting_box/index.js"),
|
||||
"@common_components": path.resolve(__dirname, "src-ui/common_components/index.js"),
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "dist"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, "index.html"),
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler"
|
||||
resolve: {
|
||||
alias: {
|
||||
"@root": path.resolve(__dirname),
|
||||
"@test_data": path.resolve(__dirname, "./test_data.js"),
|
||||
|
||||
"@ui_configs": path.resolve(__dirname, "src-ui/ui_configs.js"),
|
||||
"@scss_mixins": path.resolve(__dirname, "src-ui/common_css/mixins.scss"),
|
||||
"@store": path.resolve(__dirname, "src-ui/store.js"),
|
||||
"@images": path.resolve(__dirname, "src-ui/assets"),
|
||||
"@utils": path.resolve(__dirname, "src-ui/utils.js"),
|
||||
"@logics": path.resolve(__dirname, "src-ui/logics"),
|
||||
"@logics_common": path.resolve(__dirname, "src-ui/logics/common"),
|
||||
"@logics_main": path.resolve(__dirname, "src-ui/logics/main"),
|
||||
"@logics_configs": path.resolve(__dirname, "src-ui/logics/configs"),
|
||||
|
||||
"@setting_box": path.resolve(__dirname, "src-ui/app/config_page/setting_section/setting_box/index.js"),
|
||||
"@common_components": path.resolve(__dirname, "src-ui/common_components/index.js"),
|
||||
|
||||
// Plugins
|
||||
"@plugins_path": path.resolve(__dirname, "src-ui/plugins"),
|
||||
"@plugins_index": path.resolve(__dirname, "src-ui/plugins/plugins_index.js"),
|
||||
...plugin_aliases,
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
|
||||
// 各プラグインのエイリアスを動的に読み込む関数
|
||||
const getPluginAliases = async () => {
|
||||
const aliases = {};
|
||||
// dev_plugins 配列の各プラグインについて処理する
|
||||
for (const plugin of dev_plugins) {
|
||||
const entry_path = plugin.entry_path; // 例: "dev_plugin_subtitles"
|
||||
try {
|
||||
// エイリアス設定ファイルは各プラグインフォルダ内の "configs.js" に記述されている前提
|
||||
const pluginConfig = await import(`./src-ui/plugins/${entry_path}/plugin_configs.js`);
|
||||
if (pluginConfig.configs && pluginConfig.configs.alias) {
|
||||
for (const [alias_key, alias_relative_path] of Object.entries(pluginConfig.configs.alias)) {
|
||||
|
||||
// ホスト側の絶対パスに変換
|
||||
aliases[alias_key] = path.resolve(__dirname, "src-ui/plugins", entry_path, alias_relative_path);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading alias config for plugin ${plugin.plugin_info.plugin_id}:`, error);
|
||||
}
|
||||
}
|
||||
return aliases;
|
||||
};
|
||||
Reference in New Issue
Block a user