Merge branch 'develop'

This commit is contained in:
misyaguziya
2025-05-08 16:11:46 +09:00
112 changed files with 12328 additions and 7234 deletions

3
.gitignore vendored
View File

@@ -43,4 +43,5 @@ dist-ssr
.venv
# Customize
/build
/build
error.txt

116
README.md
View File

@@ -1,116 +0,0 @@
<div align="center">
<picture>
<source srcset="docs/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="docs/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="docs/vrct_logo.png" alt="VRCT Logo" width="50%">
</picture>
<br>
<br>
[![GitHub release](https://img.shields.io/github/v/release/misyaguziya/VRCT.svg)](https://github.com/misyaguziya/VRCT/releases)
[![Downloads](https://img.shields.io/github/downloads/misyaguziya/VRCT/total)](https://github.com/misyaguziya/VRCT/releases)
[![Licence](https://img.shields.io/github/license/misyaguziya/VRCT)](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
[![Booth](https://img.shields.io/badge/Store-Booth.pm-red)](https://misyaguziya.booth.pm/items/5155325)
[![Github Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-30363D?&logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/misyaguziya)
<h3>
Become a VRCT Supporter on:
</h3>
<a href="https://vrct-dev.fanbox.cc">
<picture>
<source srcset="docs/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="docs/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="docs/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
</picture>
</a>&emsp;&nbsp;
<a href="https://patreon.com/vrct_dev">
<picture>
<source srcset="docs/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="docs/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="docs/patreon_logo_black.png" alt="Patreon" height="22px">
</picture>
</a>&emsp;&nbsp;
<a href="https://ko-fi.com/vrct_dev">
<picture>
<img src="docs/kofi_logo.png" alt="Ko-fi" height="22px">
</picture>
</a>&emsp;&nbsp;
<br>
<picture>
<source srcset="docs/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="docs/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="docs/supporter_section_border_d.png" alt="Supporter Section Border">
</picture>
<br>
<br>
| **English** | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) |
<h3>
VRCT is software that supports VRChat conversations with translation and transcription.
</h3>
![](docs/main_window.png)
<div align="left">
# Download & Install
Download from anywhere you like.
- [Github.com](https://github.com/misyaguziya/VRCT/releases/)
- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325)
Just download and run the exe.
# What is VRCT?
VRCT is software that supports conversations between people who speak different languages by providing chat or voice translation.
These features are designed for use within VRChat.
*Although not supported, it is also used for other purposes such as watching movies.
VRCT supports your conversations with
- 💬 **Send chat to VRChat**
- 🌐 **Translation**
- 🎙 **Transcription of audio from microphone**
- 🔈 **Transcription of audio from Speaker**
# Documents
Initial setup, basic functions, and other features are also described.
- [Documents Link](https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246?pvs=4)
# How to Use (YouTube)
<div align="center">
[![](https://img.youtube.com/vi/rUTad037n8Q/0.jpg)](https://www.youtube.com/watch?v=rUTad037n8Q)
<div align="left">
# If you want to run it in python
1. Install the following version of python.
`python version 3.11.5`
2. Install package and run main.py.
```bash
./install.bat
python main.py
```
## Author
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (Main Development)
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI multilingual support)
- [レラ](https://github.com/soumt-r) (Technical Support)
- [どね](https://twitter.com/done_vrc) (Logo Design)
## Thanks to our contributors
<a href="https://github.com/misyaguziya/VRCT/graphs/contributors" target="_blank">
<img src="https://contrib.rocks/image?repo=misyaguziya/VRCT" />
</a>
---
VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc.

1
README.md Symbolic link
View File

@@ -0,0 +1 @@
docs/readmes/README.en.md

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 182 B

After

Width:  |  Height:  |  Size: 182 B

View File

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 186 B

View File

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 155 KiB

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

107
docs/readmes/README.en.md Normal file
View File

@@ -0,0 +1,107 @@
<div align="center">
<picture>
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
</picture>
<br>
<br>
[![GitHub release](https://img.shields.io/github/v/release/misyaguziya/VRCT.svg)](https://github.com/misyaguziya/VRCT/releases)
[![Downloads](https://img.shields.io/github/downloads/misyaguziya/VRCT/total)](https://github.com/misyaguziya/VRCT/releases)
[![Licence](https://img.shields.io/github/license/misyaguziya/VRCT)](https://github.com/misyaguziya/VRCT/blob/master/LICENSE)
[![Booth](https://img.shields.io/badge/Store-Booth.pm-red)](https://misyaguziya.booth.pm/items/5155325)
[![Github Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-30363D?&logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/misyaguziya)
<h3>
Become a VRCT Supporter on:
</h3>
<a href="https://vrct-dev.fanbox.cc">
<picture>
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
</picture>
</a>&emsp;&nbsp;
<a href="https://patreon.com/vrct_dev">
<picture>
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
</picture>
</a>&emsp;&nbsp;
<a href="https://ko-fi.com/vrct_dev">
<picture>
<img src="/docs/img/kofi_logo.png" alt="Ko-fi" height="22px">
</picture>
</a>&emsp;&nbsp;
<br>
<picture>
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
</picture>
<br>
<br>
| **English** | [日本語](/docs/readmes/README.ja.md) | [한국어](/docs/readmes/README.ko.md) | [繁體中文](/docs/readmes/README.zh-Hant.md) |
<h3>
VRCT is software that supports VRChat conversations with translation and transcription.
</h3>
![](/docs/img/main_window.png)
<div align="left">
# Download & Install
Download from anywhere you like.
- [Github.com](https://github.com/misyaguziya/VRCT/releases/)
- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325)
Just download and run the exe.
# What is VRCT?
VRCT is software that supports conversations between people who speak different languages by providing chat or voice translation.
These features are designed for use within VRChat.
*Although not supported, it is also used for other purposes such as watching movies.
VRCT supports your conversations with
- 💬 **Send chat to VRChat**
- 🌐 **Translation**
- 🎙 **Transcription of audio from microphone**
- 🔈 **Transcription of audio from Speaker**
# Documents
Initial setup, basic functions, and other features are also described.
- [Documents Link](https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246?pvs=4)
# How to Use (YouTube)
<div align="center">
[![](https://img.youtube.com/vi/rUTad037n8Q/0.jpg)](https://www.youtube.com/watch?v=rUTad037n8Q)
<div align="left">
## Author
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (Main Development)
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI multilingual support)
- [レラ](https://github.com/soumt-r) (Technical Advisor)
- [どね](https://twitter.com/done_vrc) (Logo Design)
## Thanks to our contributors
<a href="https://github.com/misyaguziya/VRCT/graphs/contributors" target="_blank">
<img src="https://contrib.rocks/image?repo=misyaguziya/VRCT" />
</a>
---
VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc.

View File

@@ -1,9 +1,9 @@
<div align="center">
<picture>
<source srcset="docs/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="docs/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="docs/vrct_logo.png" alt="VRCT Logo" width="50%">
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
</picture>
<br>
@@ -21,44 +21,44 @@ Become a VRCT Supporter on:
<a href="https://vrct-dev.fanbox.cc">
<picture>
<source srcset="docs/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="docs/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="docs/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
</picture>
</a>&emsp;&nbsp;
<a href="https://patreon.com/vrct_dev">
<picture>
<source srcset="docs/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="docs/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="docs/patreon_logo_black.png" alt="Patreon" height="22px">
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
</picture>
</a>&emsp;&nbsp;
<a href="https://ko-fi.com/vrct_dev">
<picture>
<img src="docs/kofi_logo.png" alt="Ko-fi" height="22px">
<img src="/docs/img/kofi_logo.png" alt="Ko-fi" height="22px">
</picture>
</a>&emsp;&nbsp;
<br>
<picture>
<source srcset="docs/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="docs/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="docs/supporter_section_border_d.png" alt="Supporter Section Border">
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
</picture>
<br>
<br>
| [English](./README.md) | **日本語** | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) |
| [English](/docs/readmes/README.en.md) | **日本語** | [한국어](/docs/readmes/README.ko.md) | [繁體中文](/docs/readmes/README.zh-Hant.md) |
<h3>
VRCTは翻訳や文字起こしでVRChatの会話をサポートするソフトウェアです。
</h3>
![](docs/main_window.png)
![](/docs/img/main_window.png)
<div align="left">
@@ -91,15 +91,6 @@ VRCTはあなたの会話を以下でサポートをします。
<div align="left">
# pythonで実行したい場合
1. 以下のバージョンのpythonをインストールしてください。
`python version 3.11.5`
2. packageのインストールとmain.pyを起動してください。
```bash
./install.bat
python main.py
```
## Author
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (メイン開発)
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI多言語対応)

View File

@@ -1,9 +1,9 @@
<div align="center">
<picture>
<source srcset="docs/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="docs/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="docs/vrct_logo.png" alt="VRCT Logo" width="50%">
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
</picture>
<br>
@@ -21,44 +21,44 @@ Become a VRCT Supporter on:
<a href="https://vrct-dev.fanbox.cc">
<picture>
<source srcset="docs/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="docs/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="docs/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
</picture>
</a>&emsp;&nbsp;
<a href="https://patreon.com/vrct_dev">
<picture>
<source srcset="docs/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="docs/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="docs/patreon_logo_black.png" alt="Patreon" height="22px">
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
</picture>
</a>&emsp;&nbsp;
<a href="https://ko-fi.com/vrct_dev">
<picture>
<img src="docs/kofi_logo.png" alt="Ko-fi" height="22px">
<img src="/docs/img/kofi_logo.png" alt="Ko-fi" height="22px">
</picture>
</a>&emsp;&nbsp;
<br>
<picture>
<source srcset="docs/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="docs/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="docs/supporter_section_border_d.png" alt="Supporter Section Border">
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
</picture>
<br>
<br>
| [English](./README.md) | [日本語](./README.ja.md) | **한국어** | [繁體中文](./README.zh-Hant.md) |
| [English](/docs/readmes/README.en.md) | [日本語](/docs/readmes/README.ja.md) | **한국어** | [繁體中文](/docs/readmes/README.zh-Hant.md) |
<h3>
VRCT는 음성인식 및 번역 기능을 통해 VRChat의 대화를 지원하는 소프트웨어입니다.
</h3>
![](docs/main_window.png)
![](/docs/img/main_window.png)
<div align="left">
@@ -91,15 +91,6 @@ VRCT는 다음과 같이 당신의 대화를 도와드려요.
<div align="left">
# python으로 실행하고 싶은 경우
1. 다음 버전의 python을 설치합니다.
`python version 3.11.5`
2. 패키지를 설치하고 main.py를 실행합니다.
```bash
./install.bat
python main.py
```
## Author
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (주요 개발)
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI 다국어 지원)

View File

@@ -1,9 +1,9 @@
<div align="center">
<picture>
<source srcset="docs/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="docs/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="docs/vrct_logo.png" alt="VRCT Logo" width="50%">
<source srcset="/docs/img/vrct_logo_white.png" media="(prefers-color-scheme: dark)" width="50%">
<source srcset="/docs/img/vrct_logo_black.png" media="(prefers-color-scheme: light)" width="50%">
<img src="/docs/img/vrct_logo.png" alt="VRCT Logo" width="50%">
</picture>
<br>
@@ -21,44 +21,44 @@ Become a VRCT Supporter on:
<a href="https://vrct-dev.fanbox.cc">
<picture>
<source srcset="docs/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="docs/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="docs/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
<source srcset="/docs/img/pixiv_fanbox_white.png" media="(prefers-color-scheme: dark)" height="18px">
<source srcset="/docs/img/pixiv_fanbox_black.png" media="(prefers-color-scheme: light)" height="18px">
<img src="/docs/img/pixiv_fanbox_black.png" alt="PIXIV FANBOX" height="18px">
</picture>
</a>&emsp;&nbsp;
<a href="https://patreon.com/vrct_dev">
<picture>
<source srcset="docs/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="docs/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="docs/patreon_logo_black.png" alt="Patreon" height="22px">
<source srcset="/docs/img/patreon_logo_white.png" media="(prefers-color-scheme: dark)" height="22px">
<source srcset="/docs/img/patreon_logo_black.png" media="(prefers-color-scheme: light)" height="22px">
<img src="/docs/img/patreon_logo_black.png" alt="Patreon" height="22px">
</picture>
</a>&emsp;&nbsp;
<a href="https://ko-fi.com/vrct_dev">
<picture>
<img src="docs/kofi_logo.png" alt="Ko-fi" height="22px">
<img src="/docs/img/kofi_logo.png" alt="Ko-fi" height="22px">
</picture>
</a>&emsp;&nbsp;
<br>
<picture>
<source srcset="docs/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="docs/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="docs/supporter_section_border_d.png" alt="Supporter Section Border">
<source srcset="/docs/img/supporter_section_border_d.png" media="(prefers-color-scheme: dark)">
<source srcset="/docs/img/supporter_section_border_l.png" media="(prefers-color-scheme: light)">
<img src="/docs/img/supporter_section_border_d.png" alt="Supporter Section Border">
</picture>
<br>
<br>
| [English](./README.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | **繁體中文** |
| [English](/docs/readmes/README.en.md) | [日本語](/docs/readmes/README.ja.md) | [한국어](/docs/readmes/README.ko.md) | **繁體中文** |
<h3>
VRCT 是一個支援 VRChat 對話翻譯和紀錄的軟體。
</h3>
![](docs/main_window.png)
![](/docs/img/main_window.png)
<div align="left">
@@ -90,15 +90,6 @@ VRCT 可以:
<div align="left">
# 原始碼啟動
1. 安裝此版本的 Python。
`python version 3.11.5`
2. 安裝 package 並啟動 main.py。
```bash
./install.bat
python main.py
```
## 作者
- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (主要開發)
- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI 多語系支援)

View File

@@ -3,8 +3,8 @@ python -m venv .venv_cuda
call .venv/Scripts/activate
python.exe -m pip install --upgrade pip
pip install -r requirements.txt
pip install --no-cache-dir --force-reinstall -r requirements.txt
call .venv_cuda/Scripts/activate
python.exe -m pip install --upgrade pip
pip install -r requirements_cuda.txt
pip install --no-cache-dir --force-reinstall -r requirements_cuda.txt

View File

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

View File

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

12264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,60 @@
{
"name": "tauri-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"setup-python": "install.bat",
"build-python": "build.bat",
"build-python-cuda": "build_cuda.bat",
"vite": "vite",
"vite-build": "vite build",
"vite-preview": "vite preview",
"tauri": "tauri",
"tauri-dev": "tauri dev",
"clean": "python clean.py",
"dev": "npm run build-python && npm run dev-ui",
"dev-cuda": "npm run build-python-cuda && npm run dev-ui",
"dev-ui": "npm-run-all --parallel vite tauri-dev",
"build": "npm run clean && npm run build-python && npm run vite-build && npm run tauri build",
"build-cuda": "npm run clean && npm run build-python-cuda && npm run vite-build && npm run tauri build",
"release": "npm run build && python zip.py --zip_name VRCT.zip",
"release-cuda": "npm run build-cuda && python zip.py --zip_name VRCT_cuda.zip",
"release-all": "npm run release && npm run release-cuda"
},
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@mui/material": "6.2.0",
"@tauri-apps/api": "1.6.0",
"@vitejs/plugin-react": "4.3.4",
"clsx": "2.1.1",
"eslint": "8.57.0",
"eslint-plugin-react": "7.37.2",
"i18next": "24.1.0",
"jotai": "2.10.3",
"js-base64": "3.7.7",
"js-yaml": "4.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "15.2.0",
"react-resizable-layout": "0.7.2"
},
"devDependencies": {
"@tauri-apps/cli": "1.6.3",
"npm-run-all": "4.1.5",
"sass": "1.79.4",
"vite": "6.0.3",
"vite-plugin-svgr": "4.3.0"
}
}
{
"name": "vrct",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"setup-python": "install.bat",
"build-python": "build.bat",
"build-python-cuda": "build_cuda.bat",
"vite": "vite",
"vite-build": "vite build",
"vite-preview": "vite preview",
"tauri": "tauri",
"tauri-dev": "tauri dev",
"clean": "python clean.py",
"dev": "npm run build-python && npm run dev-ui",
"dev-cuda": "npm run build-python-cuda && npm run dev-ui",
"dev-ui": "npm-run-all --parallel vite tauri-dev",
"build": "npm run clean && npm run build-python && npm run vite-build && npm run tauri build",
"build-cuda": "npm run clean && npm run build-python-cuda && npm run vite-build && npm run tauri build",
"release": "npm run build && python zip.py --zip_name VRCT.zip",
"release-cuda": "npm run build-cuda && python zip.py --zip_name VRCT_cuda.zip",
"release-all": "npm run release && npm run release-cuda"
},
"dependencies": {
"@babel/standalone": "7.27.0",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@mui/material": "7.0.2",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-fs": "2.2.1",
"@tauri-apps/plugin-global-shortcut": "2.2.0",
"@tauri-apps/plugin-http": "2.4.3",
"@tauri-apps/plugin-opener": "2.2.6",
"@tauri-apps/plugin-shell": "2.2.1",
"clsx": "2.1.1",
"i18next": "25.0.1",
"jotai": "2.12.3",
"js-base64": "3.7.7",
"jszip": "3.10.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-error-boundary": "5.0.0",
"react-i18next": "15.5.1",
"react-resizable-layout": "0.7.2",
"sass": "1.79.4",
"semver": "7.7.1"
},
"devDependencies": {
"@rollup/plugin-yaml": "4.1.2",
"@tauri-apps/cli": "2.5.0",
"@vitejs/plugin-react": "4.4.1",
"eslint": "9.25.1",
"eslint-plugin-react": "7.37.5",
"npm-run-all": "4.1.5",
"source-map": "0.7.4",
"vite": "6.3.4",
"vite-plugin-svgr": "4.3.0"
}
}

View File

@@ -1,6 +1,6 @@
torch==2.2.2
faster-whisper==1.0.3
ctranslate2==4.3.1
torch==2.7.0
faster-whisper==1.1.1
ctranslate2==4.6.0
transformers==4.40.2
pillow == 10.0.0
PyAudioWPatch == 0.2.12.6

View File

@@ -1,7 +1,7 @@
torch==2.2.2
--extra-index-url https://download.pytorch.org/whl/cu121
faster-whisper==1.0.3
ctranslate2==4.3.1
torch==2.7.0
--extra-index-url https://download.pytorch.org/whl/cu128
faster-whisper==1.1.1
ctranslate2==4.6.0
transformers==4.40.2
pillow == 10.0.0
PyAudioWPatch == 0.2.12.6

View File

@@ -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):
@@ -944,7 +956,7 @@ class Config:
def init_config(self):
# Read Only
self._VERSION = "3.0.5"
self._VERSION = "3.1.0"
if getattr(sys, 'frozen', False):
self._PATH_LOCAL = os_path.dirname(sys.executable)
else:
@@ -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

View File

@@ -321,7 +321,14 @@ class Controller:
elif isinstance(message, str) and len(message) > 0:
translation = []
transliteration = []
if model.detectRepeatReceiveMessage(message):
if model.checkKeywords(message):
self.run(
200,
self.run_mapping["word_filter"],
{"message":f"Detected by word filter: {message}"},
)
return
elif model.detectRepeatReceiveMessage(message):
return
elif config.ENABLE_TRANSLATION is False:
pass
@@ -447,11 +454,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 +1068,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}

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ class OSCHandler:
self.osc_server_ip_address = ip_address
self.http_port = None
self.osc_server_port = None
self.browser = None
def setOscIpAddress(self, ip_address:str) -> None:
self.osc_ip_address = ip_address
@@ -45,18 +46,26 @@ class OSCHandler:
def getOSCParameterValue(self, address:str) -> Any:
value = None
try:
browser = OSCQueryBrowser()
sleep(1)
service = browser.find_service_by_name(self.osc_server_name)
# browserインスタンスを再利用し、毎回の生成と破棄を避ける
if self.browser is None:
self.browser = OSCQueryBrowser()
sleep(1) # 初回のみスリープ
service = self.browser.find_service_by_name(self.osc_server_name)
if service is not None:
osc_query_client = OSCQueryClient(service)
mute_self_node = osc_query_client.query_node(address)
value = mute_self_node.value[0]
browser.zc.close()
browser.browser.cancel()
except Exception:
errorLogging()
# エラー発生時にbrowserをリセットして次回再初期化
if self.browser is not None:
try:
self.browser.zc.close()
self.browser.browser.cancel()
except Exception:
pass
self.browser = None
return value
def getOSCParameterMuteSelf(self) -> bool:
@@ -82,7 +91,8 @@ class OSCHandler:
sleep(1)
def oscServerServe(self) -> None:
self.osc_server.serve_forever(2)
# ポーリング間隔を長くして2秒から10秒にCPUの使用率を削減
self.osc_server.serve_forever(10)
def oscServerStop(self) -> None:
if isinstance(self.osc_server, osc_server.ThreadingOSCUDPServer):
@@ -91,6 +101,14 @@ class OSCHandler:
if isinstance(self.osc_query_service, OSCQueryService):
self.osc_query_service.http_server.shutdown()
self.osc_query_service = None
# browserがある場合はクリーンアップ
if self.browser is not None:
try:
self.browser.zc.close()
self.browser.browser.cancel()
except Exception:
pass
self.browser = None
if __name__ == "__main__":
handler = OSCHandler()

3765
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,27 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "vrct_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "1", features = [] }
tauri-build = { version = "=2.2.0", 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"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
font-kit = "0.14.2"
window-shadows = { git = "https://github.com/tauri-apps/window-shadows.git" }
tauri = { version = "=2.5.1", features = ["devtools"] }
tauri-plugin-opener = "=2.2.6"
serde = { version = "=1.0.219", features = ["derive"] }
serde_json = "=1.0.140"
tauri-plugin-fs = "=2.2.1"
tauri-plugin-http = "=2.4.3"
tauri-plugin-shell = "2.2.1"
font-kit = "=0.14.2"
reqwest = "=0.12.15"
base64 = "=0.22.1"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "=2.2.0"

View File

@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}

View File

@@ -0,0 +1,63 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "vrct-capability",
"description": "VRCT main window capabilities",
"windows": ["main"],
"permissions": [
"core:window:default",
"core:window:allow-start-dragging",
"core:window:allow-close",
"core:window:allow-set-position",
"core:window:allow-set-size",
"core:window:allow-set-always-on-top",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-set-focus",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-register-all",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"fs:default",
"fs:allow-write-file",
"fs:allow-remove",
"fs:allow-resource-read-recursive",
"fs:allow-resource-meta-recursive",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$RESOURCE/plugins/**" },
{ "path": "src-tauri/target/debug/plugins/**" }
]
},
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.github.com/repos/**" },
{ "url": "https://github.com/**" },
{ "url": "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/vrct_plugins_list.json" },
{ "url": "https://raw.githubusercontent.com/ShiinaSakamoto/vrct_plugins_list/main/dev_vrct_plugins_list.json" }
]
},
"shell:allow-open",
"shell:allow-stdin-write",
{
"identifier": "shell:allow-spawn",
"allow": [
{ "name": "bin/VRCT-sidecar", "sidecar": true, "args": true }
]
},
{
"identifier": "shell:allow-execute",
"allow": [
{ "name": "bin/VRCT-sidecar", "sidecar": true, "args": true }
]
}
]
}

View File

@@ -588,25 +588,9 @@ Section Install
!insertmacro CheckIfAppIsRunning
; ; Copy main executable
; File "${MAINBINARYSRCPATH}"
; ; Copy resources
; {{#each resources_dirs}}
; CreateDirectory "$INSTDIR\\{{this}}"
; {{/each}}
; {{#each resources}}
; File /a "/oname={{this.[1]}}" "{{unescape-dollar-sign @key}}"
; {{/each}}
; ; Copy external binaries
; {{#each binaries}}
; File /a "/oname={{this}}" "{{unescape-dollar-sign @key}}"
; {{/each}}
!addplugindir "..\..\..\..\nsis\plugins\x86-unicode"
; 指定のURLからファイルをダウンロード
!define SOFTWARE_RELEASE_URL "https://huggingface.co/misyaguziya/VRCT/resolve/main"
!define SOFTWARE_RELEASE_URL "https://huggingface.co/ms-software/VRCT/resolve/main"
!define SOFTWARE_DOWNLOAD_FILENAME "VRCT.zip"
Var /GLOBAL i
Var /GLOBAL cmder_dl

View 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.

64
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,64 @@
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let _main_window = app.get_webview_window("main").unwrap(); // `main_window` is declared here for all builds
#[cfg(debug_assertions)]
{ _main_window.open_devtools(); }
Ok(())
})
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![get_font_list, download_zip_asset])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
use font_kit::{source::SystemSource};
use std::collections::HashSet;
#[tauri::command]
async fn get_font_list() -> Vec<String> {
let source = SystemSource::new();
let mut font_families = HashSet::new();
if let Ok(fonts) = source.all_fonts() {
for font in fonts {
if let Ok(info) = font.load() {
font_families.insert(info.family_name().to_string());
}
}
}
font_families.into_iter().collect()
}
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
#[tauri::command]
async fn download_zip_asset(url: String) -> Result<String, String> {
use reqwest;
let client = reqwest::Client::new();
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))?;
Ok(BASE64.encode(&bytes))
}

View File

@@ -1,48 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::Manager;
use window_shadows::set_shadow;
fn main() {
tauri::Builder::default()
.setup(|app| {
let main_window = app.get_window("main").unwrap(); // `main_window` is declared here for all builds
#[cfg(debug_assertions)]
{ main_window.open_devtools(); }
#[cfg(any(windows, target_os = "macos"))]
set_shadow(main_window, true).unwrap();
Ok(())
})
.on_window_event(|event| { // This is for fix the bug that the window scaling issue when dragging between monitors.
if let tauri::WindowEvent::ScaleFactorChanged { new_inner_size, .. } = event.event() {
event.window().set_size(tauri::Size::Physical(*new_inner_size)).unwrap();
}
})
.invoke_handler(tauri::generate_handler![get_font_list])
.run(tauri::generate_context!())
.expect("error while running tauri application");
vrct_lib::run()
}
use font_kit::{source::SystemSource};
use std::collections::HashSet;
#[tauri::command]
async fn get_font_list() -> Vec<String> {
let source = SystemSource::new();
let mut font_families = HashSet::new();
if let Ok(fonts) = source.all_fonts() {
for font in fonts {
if let Ok(info) = font.load() {
font_families.insert(info.family_name().to_string());
}
}
}
font_families.into_iter().collect()
}

View File

@@ -1,44 +1,16 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "VRCT",
"version": "3.0.0",
"identifier": "com.vrct.app",
"build": {
"beforeDevCommand": "",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "",
"devPath": "http://localhost:1420",
"distDir": "../dist"
"frontendDist": "../dist"
},
"package": {
"productName": "VRCT",
"version": "3.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"window": {
"all": false,
"setAlwaysOnTop": true,
"setFocus": true,
"setDecorations": true,
"close": true,
"hide": true,
"setPosition": true,
"setSize": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true
},
"globalShortcut": {
"all": true
},
"shell": {
"all": false,
"open": true,
"sidecar": true,
"scope": [
{ "name": "bin/VRCT-sidecar", "sidecar": true, "args": true }
]
}
},
"app": {
"enableGTKAppId": false,
"windows": [{
"title": "VRCT",
"center": true,
@@ -47,36 +19,40 @@
"minWidth": 400,
"minHeight": 200,
"transparent": true,
"decorations": false
"decorations": false,
"shadow": false
}],
"security": { "csp": null },
"bundle": {
"active": true,
"targets": "nsis",
"identifier": "com.vrct.dev",
"publisher": "m's software",
"copyright": "Copyright m's software",
"shortDescription": "VRCT",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"bin/VRCT-sidecar"
],
"resources": {
"bin/_internal": "_internal"
},
"windows": {
"nsis": {
"template": "nsis/template.nsi",
"license": "../LICENSE",
"installMode": "currentUser",
"displayLanguageSelector": true
}
"security": {
"csp": null,
"capabilities": ["default", "vrct-capability"]
}
},
"bundle": {
"active": true,
"targets": "nsis",
"publisher": "m's software",
"copyright": "Copyright m's software",
"licenseFile": "../LICENSE",
"shortDescription": "VRCT",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"bin/VRCT-sidecar"
],
"resources": {
"bin/_internal": "_internal",
"plugins": "plugins"
},
"windows": {
"nsis": {
"template": "nsis/template.nsi",
"installMode": "currentUser",
"displayLanguageSelector": true
}
}
}

View File

@@ -9,6 +9,8 @@ import {
UiSizeController,
FontFamilyController,
TransparencyController,
CornerRadiusController,
PluginsController,
} from "./_app_controllers/index.js";
import { WindowTitleBar } from "./window_title_bar/WindowTitleBar";
@@ -20,39 +22,45 @@ 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();
const { currentIsBackendReady } = useIsBackendReady();
const { WindowGeometryController } = useWindow();
const { i18n } = useTranslation();
return (
<div className={styles.container}>
<KeyEventController />
<StartPythonController />
<GlobalHotKeyController />
<UiLanguageController />
<ConfigPageCloseTriggerController />
<UiSizeController />
<FontFamilyController />
<TransparencyController />
<WindowGeometryController />
<AppErrorBoundary >
<KeyEventController />
<StartPythonController />
<GlobalHotKeyController />
<UiLanguageController />
<ConfigPageCloseTriggerController />
<UiSizeController />
<FontFamilyController />
<TransparencyController />
<CornerRadiusController />
{(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>
);
};
const Contents = () => {
const { WindowGeometryController } = useWindow();
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
return (
<>
<WindowGeometryController />
<PluginsController />
<WindowTitleBar />
{currentIsSoftwareUpdating.data === false
?

View File

@@ -0,0 +1,53 @@
import { useState, useEffect } from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
export const CornerRadiusController = () => {
const [is_win11, setIsWin11] = useState(false);
const [is_maximized, setIsMaximized] = useState(false);
// OS 判定Win11 なら platformVersion の major ≥13
useEffect(() => {
if (navigator.userAgentData?.getHighEntropyValues) {
navigator.userAgentData
.getHighEntropyValues(["platformVersion"])
.then(({ platformVersion }) => {
const major = parseInt(platformVersion.split(".")[0], 10);
setIsWin11(major >= 13);
})
.catch(() => {
setIsWin11(false);
})
} else {
// フォールバックで Win10 扱い
setIsWin11(false);
}
}, [])
useEffect(() => {
let unlisten;
const setup = async () => {
const window = await getCurrentWindow();
// 初期状態取得
setIsMaximized(await window.isMaximized());
// リサイズ時にも再取得
const updateMax = () => window.isMaximized().then(setIsMaximized);
unlisten = await window.listen("tauri://resize", updateMax);
}
setup();
return () => {
if (unlisten) {
unlisten();
}
}
}, []);
// 角丸の適用
useEffect(() => {
const radius = is_win11 && !is_maximized ? "10px" : "0";
document.body.style.borderRadius = radius;
}, [is_win11, is_maximized]);
return null;;
}

View 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 />
</>
);
};

View File

@@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/tauri";
import { invoke } from "@tauri-apps/api/core";
import { useEffect, useRef } from "react";
import { useStartPython } from "@logics/useStartPython";
import { useStdoutToPython } from "@logics/useStdoutToPython";

View File

@@ -5,4 +5,6 @@ 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";
export { CornerRadiusController } from "./CornerRadiusController";

View File

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

View File

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

View File

@@ -0,0 +1,205 @@
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 });
}
}
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;
};

View File

@@ -24,7 +24,8 @@
html, body {
height: 100%;
font-family: var(--font_family); /* If not found the font family where 'root:' that is selected by user*/
border-radius: 1.8rem;
border-radius: 10px; /* fixed by px */
overflow: hidden;
}

View File

@@ -19,6 +19,7 @@
--primary_600_color_44: #36877744;
--sent_400_color: #6197b4;
--sent_300_color: #6197b4;
--received_300_color: #a861b4;
--error_bc_color: #bb4448;
--error_bc_active_color: #9c3938;
@@ -65,4 +66,5 @@
--config_page_topbar_height: 8rem;
--font_family: "Yu Gothic UI";
}
}
/* https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors */

View File

@@ -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":

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
import { useTranslation } from "react-i18next";
import CircularProgress from "@mui/material/CircularProgress";
import styles from "./DownloadModels.module.scss";
import {
RadioButton,
} from "../index";
import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton";
export const DownloadModels = (props) => {
const options = props.options.map(item => ({
@@ -19,47 +17,9 @@ export const DownloadModels = (props) => {
options={options}
checked_variable={props.checked_variable}
column={true}
ChildComponent={ModelSelector}
ChildComponent={_DownloadButton}
downloadStartFunction={props.downloadStartFunction}
/>
</>
);
};
const ModelSelector = ({option, ...props}) => {
const { t } = useTranslation();
const renderContent = () => {
const circular_progress = Math.floor(option.progress / 10) * 10;
switch (true) {
case option.progress !== null:
return (
<>
<CircularProgress
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
value={circular_progress}
size="3rem"
sx={{ color: "var(--primary_300_color)" }}
/>
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
</>
);
case option.is_pending:
return <CircularProgress size="3rem" sx={{ color: "var(--dark_600_color)" }}/>;
case !option.is_downloaded:
return (
<button
className={styles.download_button}
onClick={() => props.downloadStartFunction(option.id)}
>
<p className={styles.download_button_label}>{t("config_page.model_download_button_label")}</p>
</button>
);
default:
return null;
}
};
return <div className={styles.download_container}>{renderContent()}</div>;
};
};

View File

@@ -1,30 +0,0 @@
@import "@scss_mixins";
.download_container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 8rem;
}
.download_button {
pointer-events: auto;
background-color: var(--dark_800_color);
padding: 0.8rem;
flex-shrink: 0;
&:hover {
background-color: var(--dark_750_color);
}
&:active {
background-color: var(--dark_800_color);
}
}
.download_button_label {
font-size: 1.2rem;
}
.progress_label {
position: absolute;
font-size: 1rem;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
}
.unavailable_text {
padding: 1rem;
font-size: 1.2rem;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,174 @@
import { useEffect, useRef, useState, useCallback } 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";
import ExternalLink from "@images/external_link.svg?react";
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,
};
const homepage_link = plugin.latest_plugin_info?.homepage_link;
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> */}
{homepage_link && <HomepageLinkButton homepage_link={homepage_link}/>
}
</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>
);
};
const HomepageLinkButton = ({ homepage_link, speed = 40 /* px/s */ }) => {
const containerRef = useRef(null);
const textRef = useRef(null);
const [inlineStyle, setInlineStyle] = useState({});
const handleMouseEnter = useCallback(() => {
const container = containerRef.current;
const text = textRef.current;
if (!container || !text) return;
const overflow = text.scrollWidth - container.clientWidth;
if (overflow > 0) {
const duration = overflow / speed;
setInlineStyle({
transform: `translateX(-${overflow}px)`,
transition: `transform ${duration}s linear`,
});
}
}, [speed]);
const handleMouseLeave = useCallback(() => {
setInlineStyle({
transform: 'translateX(0)',
transition: 'transform 0.3s ease-out',
});
}, []);
return (
<div className={styles.open_homepage_button_wrapper}>
<a
className={styles.open_homepage_button}
href={homepage_link}
target="_blank"
rel="noreferrer"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={styles.text_container} ref={containerRef}>
<p
className={styles.open_homepage_text}
ref={textRef}
style={inlineStyle}
>
{homepage_link}
</p>
</div>
<ExternalLink className={styles.external_link_svg} />
</a>
</div>
);
};

View File

@@ -0,0 +1,99 @@
.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;
// }
.open_homepage_button_wrapper {
width: 100%;
}
.open_homepage_button {
padding: 0.6rem 0;
display: flex;
align-items: center;
gap: 1rem;
cursor: pointer;
text-decoration: none;
&:hover .external_link_svg {
color: var(--dark_200_color);
}
&:hover .open_homepage_text {
border-bottom: 0.1rem solid var(--dark_500_color);
}
}
.text_container {
position: relative;
overflow: hidden;
flex: 1; /* テキスト部分をアイコンまで伸ばす */
}
.open_homepage_text {
margin: 0; /* pタグのデフォルトマージン除去 */
font-size: 1.4rem;
color: var(--sent_400_color);
white-space: nowrap;
display: inline-block;
transform: translateX(0);
border-bottom: 0.1rem solid var(--sent_400_color);
}
.external_link_svg {
flex-shrink: 0;
width: 1.6rem;
color: var(--dark_500_color);
}

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
@keyframes revealTopImg {
0% {

View File

@@ -1,5 +1,6 @@
import React, { useState, useCallback, useEffect } from "react";
import clsx from "clsx";
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import styles from "./SupportersWrapper.module.scss";
import { shuffleArray, randomIntMinMax, randomMinMax } from "@utils";
@@ -261,18 +262,39 @@ const SupporterLabelComponent = ({ item, target_plan, chato_src }) => {
const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
const period_data = extractKeys(settings, calc_support_period);
const offset = {
popper: {
sx: {
[`&.${tooltipClasses.popper}[data-popper-placement*="top"] .${tooltipClasses.tooltip}`]: { marginBottom: "0.2em" },
}
}
};
return (
<div className={styles.supporter_period_container}>
{Object.entries(period_data).map(([key, item], index) => {
if (item === "") return null;
const class_name = clsx(styles.period_box, {
const period_box_class_name = clsx(styles.period_box, {
[styles.mogu_bar]: item === "mogu_2000",
[styles.mochi_bar]: item === "mochi_1000",
[styles.fuwa_bar]: item === "fuwa_500",
[styles.basic_bar]: item === "basic_300",
});
return <div key={index} className={class_name}></div>;
return (
<Tooltip
key={index}
title={
<p className={styles.tooltip_period_label}>{key}</p>
}
placement="top"
slotProps={offset}
>
<div className={styles.period_box_wrapper}>
<div className={period_box_class_name}></div>
</div>
</Tooltip>
);
})}
</div>
);

View File

@@ -13,7 +13,7 @@
align-content: start;
flex-wrap: wrap;
column-gap: 1.8rem;
row-gap: 1rem;
row-gap: 0.4rem;
}
.supporter_image_container {
@@ -21,12 +21,14 @@
width: 18rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.supporter_image_wrapper {
position: relative;
overflow: hidden;
&:hover .supporter_icon_wrapper{
transform: rotate(360deg);
}
}
.supporter_image {
@@ -54,7 +56,8 @@
height: 1.4rem;
}
}
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
.supporter_icon_wrapper {
height: 4rem;
aspect-ratio: 1 /1;
@@ -62,6 +65,7 @@
background-color: #ffffff;
overflow: hidden;
position: relative;
transition: transform 0.6s $progress_ease;
}
.supporter_icon {
@@ -227,6 +231,12 @@
background-color: var(--dark_800_color);
}
}
.period_box_wrapper {
padding: 0.3rem 0 0.4rem 0;
}
.tooltip_period_label {
font-size: 1.4rem;
}
.progress_bar {

View File

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

View File

@@ -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}>
@@ -44,7 +45,9 @@ const Tab = (props) => {
const getLabel = () => {
if (props.tab_id === "vr") return "VR";
if (props.tab_id === "supporters") return "Supporters";
if (props.tab_id === "supporters") return (
<>Supporters<span className={styles.crown_emoji}>👑</span></>
);
if (props.tab_id === "about_vrct") return "About VRCT";
return t(`config_page.side_menu_labels.${props.tab_id}`);
};

View File

@@ -60,9 +60,18 @@
}
.tab_text {
overflow: hidden;
// overflow: hidden;
font-size: 1.6rem;
text-overflow: ellipsis;
// text-overflow: ellipsis;
position: relative;
}
.crown_emoji {
font-size: 1.6rem;
position: absolute;
top: 40%;
left: 100%;
transform: translateY(-50%);
padding-left: 0.4rem;
}
.separated_tabs_wrapper {

View File

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

View File

@@ -0,0 +1,91 @@
import { useState } from "react";
import { getCurrentWindow } 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 asyncClose = async () => {
const appWindow = await getCurrentWindow();
appWindow.close();
};
return (
<button className={styles.close_button_wrapper} onClick={asyncClose}>
<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;
};

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

View File

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

View File

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

View File

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

View File

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

View 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;
})}
</>
);
};

View File

@@ -5,7 +5,7 @@ import { useMessage } from "@logics_common";
import { useSendMessageButtonType, useEnableAutoClearMessageInputBox } from "@logics_configs";
import { useMessageLogScroll } from "@logics_main";
import { store } from "@store";
import { appWindow } from "@tauri-apps/api/window";
import { getCurrentWindow } from "@tauri-apps/api/window";
export const MessageInputBox = () => {
const [message_history, setMessageHistory] = useState([]);
@@ -41,6 +41,7 @@ export const MessageInputBox = () => {
const onSubmitFunction = (e) => {
e.preventDefault();
// const appWindow = getCurrentWindow();
// appWindow.minimize();
if (!currentMessageInputValue.data.trim()) return updateMessageInputValue("");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { clsx } from "clsx";
import clsx from "clsx";
import Snackbar from "@mui/material/Snackbar";
import Slide from "@mui/material/Slide";

View File

@@ -4,7 +4,7 @@ import { StartUpProgressContainer } from "./start_up_progress_container/StartUpP
import { DownloadModelsContainer } from "./download_models_container/DownloadModelsContainer/";
import MegaphoneSvg from "@images/megaphone.svg?react";
import XMarkSvg from "@images/cancel.svg?react";
import { appWindow } from "@tauri-apps/api/window";
import { getCurrentWindow } from "@tauri-apps/api/window";
import clsx from "clsx";
export const SplashComponent = () => {
@@ -73,12 +73,13 @@ const AnnouncementsContainer = () => {
const CloseButtonContainer = () => {
const close = () => {
const asyncClose = async () => {
const appWindow = await getCurrentWindow();
appWindow.close();
};
return (
<button className={styles.close_button_wrapper} onClick={close}>
<button className={styles.close_button_wrapper} onClick={asyncClose}>
<div className={styles.close_button}>
<XMarkSvg className={styles.x_mark_svg}/>
</div>

View File

@@ -51,6 +51,7 @@
}
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
.progress_bar {
height: 8px;
transition: width 0.3s $progress_ease;

View File

@@ -5,15 +5,17 @@ import SquareSvg from "@images/square.svg?react";
import LineSvg from "@images/line.svg?react";
import VrctSvg from "@images/vrct.svg?react";
import { appWindow } from "@tauri-apps/api/window";
import { getCurrentWindow } from "@tauri-apps/api/window";
export const WindowTitleBar = () => {
const minimize = () => {
const asyncMinimize = async () => {
const appWindow = await getCurrentWindow();
appWindow.minimize();
};
const maximize = async () => {
const asyncMaximize = async () => {
const appWindow = await getCurrentWindow();
const maximizeState = await appWindow.isMaximized();
if (!maximizeState) {
appWindow.maximize();
@@ -22,7 +24,8 @@ export const WindowTitleBar = () => {
}
};
const close = () => {
const asyncClose = async () => {
const appWindow = await getCurrentWindow();
appWindow.close();
};
@@ -34,13 +37,13 @@ export const WindowTitleBar = () => {
</div>
<div className={styles.window_control_wrapper}>
<div className={styles.minimize_button} onClick={minimize}>
<div className={styles.minimize_button} onClick={asyncMinimize}>
<LineSvg className={styles.line_svg}/>
</div>
<div className={styles.maximize_button} onClick={maximize}>
<div className={styles.maximize_button} onClick={asyncMaximize}>
<SquareSvg className={styles.square_svg}/>
</div>
<div className={styles.close_button} onClick={close}>
<div className={styles.close_button} onClick={asyncClose}>
<XMarkSvg className={styles.x_mark_svg}/>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 41 KiB

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
View 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

View File

@@ -1,4 +1,4 @@
import { clsx } from "clsx";
import clsx from "clsx";
import styles from "./Checkbox.module.scss";
export const Checkbox = ({
checkboxId,

View File

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

View File

@@ -0,0 +1,27 @@
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
export const useFetch = () => {
const asyncTauriFetchGithub = async (url, {return_row = false} = {}) => {
console.log("tauriFetch", url);
const response = await tauriFetch(url, {
method: "GET",
headers: {
"Accept": "application/vnd.github+json",
"User-Agent": "VRCTPluginApp"
}
});
if (response.status !== 200) {
throw new Error(url, "Failed to fetch, response: " + response);
}
if (return_row === true) return await response;
return await response.json();
};
return {
asyncTauriFetchGithub,
};
};

View File

@@ -1,10 +0,0 @@
import { useStore_IsSoftwareUpdateAvailable } from "@store";
export const useIsSoftwareUpdateAvailable = () => {
const { currentIsSoftwareUpdateAvailable, updateIsSoftwareUpdateAvailable } = useStore_IsSoftwareUpdateAvailable();
return {
currentIsSoftwareUpdateAvailable,
updateIsSoftwareUpdateAvailable,
};
};

View 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,
};
};

Some files were not shown because too many files have changed in this diff Show More