refactor!: rebrand product from DazedMTLTool to DazedTL
- Rename desktop launcher, window titles, About, and User-Agents - Point QSettings at DazedTL with one-time migration from legacy app key - Update README, Guide, skills, and launcher/script copy BREAKING CHANGE: QSettings and desktop id now use DazedTL (legacy keys migrated once)
This commit is contained in:
parent
41691879dc
commit
4d1b01c870
30 changed files with 4353 additions and 4311 deletions
|
|
@ -1,10 +1,10 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=DazedMTLTool
|
||||
GenericName=Translation Tool
|
||||
Name=DazedTL
|
||||
GenericName=AI Translation Tool
|
||||
Comment=AI translation tool for visual novels and RPG games
|
||||
Exec=bash -c 'exec "$(cd "$(dirname "%k")" && pwd)/scripts/launch.sh"'
|
||||
Icon=%k/assets/icon.png
|
||||
Terminal=false
|
||||
Categories=Development;Utility;
|
||||
StartupWMClass=DazedMTLTool
|
||||
StartupWMClass=DazedTL
|
||||
12
README.md
12
README.md
|
|
@ -1,10 +1,10 @@
|
|||
# DazedMTLTool
|
||||
# DazedTL
|
||||
|
||||
An AI-powered game translation tool with a GUI. Translate RPG Maker, Ren'Py, Tyrano, Wolf RPG, Kirikiri, and other game engines from Japanese to English using GPT, Gemini, [Mistral](https://docs.mistral.ai/api), or other compatible AI models.
|
||||
|
||||
## Credits
|
||||
|
||||
- **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back. A curated copy is bundled offline in `util/ace/offline/` and updates with DazedMTLTool.
|
||||
- **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back. A curated copy is bundled offline in `util/ace/offline/` and updates with DazedTL.
|
||||
- **Sakura & Kao_SSS** — TL Inspector (`util/tl_inspector/`) — in-game translation source inspector and live-edit plugin for RPG Maker MV/MZ playtesting.
|
||||
- **Len** — [Forge](https://gitgud.io/zero64801/forge-mvmz) MV/MZ playtest plugin (`util/forge/`), Mistral API support (provider integration and adaptive rate limiting), and batch translation mode (Anthropic Message Batches API).
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ This means Python wasn't added to your PATH. You have two options:
|
|||
### 1. Download the Tool
|
||||
|
||||
1. Click the green **Code** button at the top of this page and select **Download ZIP**.
|
||||
2. Extract the ZIP to a folder of your choice (e.g., `C:\DazedMTLTool`).
|
||||
2. Extract the ZIP to a folder of your choice (e.g., `C:\DazedTL`).
|
||||
|
||||
### 2. Set Up Your API Key
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ This means Python wasn't added to your PATH. You have two options:
|
|||
|
||||
**Windows:** Double-click `START.bat`. It will create a virtual environment, install dependencies, and open the GUI.
|
||||
|
||||
**Linux/macOS:** Run `./START.sh`, or double-click `DazedMTLTool.desktop` (choose **Allow Launching** when your file manager asks). From then on, either method works.
|
||||
**Linux/macOS:** Run `./START.sh`, or double-click `DazedTL.desktop` (choose **Allow Launching** when your file manager asks). From then on, either method works.
|
||||
|
||||
That's it! Use the same launcher each time you want to open the tool.
|
||||
|
||||
|
|
@ -350,7 +350,7 @@ After Step 0 extract, the **Database** tab discovery report classifies your game
|
|||
| **Classic RPG** (most dialogue in maps/common events) | Names → foundation DB → maps/events (skip narrative DB if none) |
|
||||
| **Hybrid** | Names → foundation DB → narrative DB → maps/events |
|
||||
|
||||
> **`wolf` binary:** Prebuilt `wolf` CLIs for Windows and Linux are bundled offline under `util/wolfdawn/bin/<platform>/`, so no toolchain or build step is needed. They update when you update DazedMTLTool. If your platform's binary is missing, update the tool or ask the maintainer to refresh the bundled copy.
|
||||
> **`wolf` binary:** Prebuilt `wolf` CLIs for Windows and Linux are bundled offline under `util/wolfdawn/bin/<platform>/`, so no toolchain or build step is needed. They update when you update DazedTL. If your platform's binary is missing, update the tool or ask the maintainer to refresh the bundled copy.
|
||||
|
||||
> **Legacy modules:** The older `Wolf RPG` / `Wolf RPG 2` modules (configured in the Engine Config tab) still exist for edge cases, but the WolfDawn workflow above is the recommended path.
|
||||
|
||||
|
|
@ -378,7 +378,7 @@ You can also select a block of text, right-click, and choose **Copilot → Fix /
|
|||
|
||||
### Modifying Tool Modules
|
||||
|
||||
Open the DazedMTLTool folder in VSCode and ask Copilot to make changes to the translation modules. Examples:
|
||||
Open the DazedTL folder in VSCode and ask Copilot to make changes to the translation modules. Examples:
|
||||
|
||||
- *"Add a new regex pattern to skip lines that start with //"*
|
||||
- *"Change the wordwrap logic to break on full-width punctuation"*
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
setlocal EnableDelayedExpansion
|
||||
|
||||
echo ==========================================
|
||||
echo DazedMTLTool Startup Script
|
||||
echo DazedTL Startup Script
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ echo.
|
|||
|
||||
:: Launch the GUI
|
||||
echo ==========================================
|
||||
echo Launching DazedMTLTool GUI...
|
||||
echo Launching DazedTL GUI...
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
|
|
|
|||
6
START.sh
6
START.sh
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# DazedMTLTool startup script for Linux/macOS
|
||||
# DazedTL startup script for Linux/macOS
|
||||
|
||||
set -o pipefail
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ ensure_vocab_file() {
|
|||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo " DazedMTLTool Startup Script"
|
||||
echo " DazedTL Startup Script"
|
||||
echo "=========================================="
|
||||
echo
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ fi
|
|||
echo
|
||||
|
||||
echo "=========================================="
|
||||
echo " Launching DazedMTLTool GUI..."
|
||||
echo " Launching DazedTL GUI..."
|
||||
echo "=========================================="
|
||||
echo
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Welcome to DazedMTLTool
|
||||
# Welcome to DazedTL
|
||||
|
||||
DazedMTLTool translates Japanese games into English with an AI API.
|
||||
DazedTL translates Japanese games into English with an AI API.
|
||||
It has the strongest support for **RPG Maker** (MV / MZ / Ace) and **Wolf RPG** (WolfDawn),
|
||||
plus modules for Ren'Py, Tyrano, Kirikiri, CSV, and more.
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ Use either:
|
|||
### Example: open the game in Cursor
|
||||
|
||||
1. In Cursor: **File → Open Folder** and select the game root (where `Game.exe` or Wolf `Data` lives).
|
||||
2. In DazedMTLTool Workflow Step 2, click **Copy Project Setup**.
|
||||
2. In DazedTL Workflow Step 2, click **Copy Project Setup**.
|
||||
3. Paste into Cursor chat with the game files available (or `@`-mention key JSON files).
|
||||
4. Paste the agent's labeled blocks back into the Workflow editors.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ You just downloaded a small MV game. You want English dialogue, menus that make
|
|||
|
||||
### 1. Get the tool ready
|
||||
|
||||
1. Launch DazedMTLTool (opens on this Guide).
|
||||
1. Launch DazedTL (opens on this Guide).
|
||||
2. Open **Configuration**, set your API key and model, save.
|
||||
3. Open the game folder in Cursor so the agent can read JSON later.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# DazedMTLTool — Project Setup
|
||||
# DazedTL — Project Setup
|
||||
|
||||
You are analysing a Japanese game project to produce configuration artifacts for DazedMTLTool.
|
||||
You are analysing a Japanese game project to produce configuration artifacts for DazedTL.
|
||||
Work in the game repository. Scan files; do not invent content you did not see.
|
||||
|
||||
---
|
||||
|
|
@ -99,7 +99,7 @@ Output as short imperative bullets suitable to paste into `skills/quirks.md`.
|
|||
### Game skill rules (for `game_skill` block)
|
||||
|
||||
Produce the per-game translation skill saved at `skills/game.md`.
|
||||
DazedMTLTool **merges this file into the translation system prompt** (before quirks).
|
||||
DazedTL **merges this file into the translation system prompt** (before quirks).
|
||||
|
||||
**Translation Frame only** (one compact line per field; evidence-based):
|
||||
- `世界観 (Theme / setting)` - genre, world type, core atmosphere
|
||||
|
|
@ -195,7 +195,7 @@ Output as short imperative bullets suitable to paste into `skills/quirks.md`.
|
|||
### Game skill rules (for `game_skill` block)
|
||||
|
||||
Produce the per-game translation skill saved at `skills/game.md`.
|
||||
DazedMTLTool **merges this file into the translation system prompt** (before quirks).
|
||||
DazedTL **merges this file into the translation system prompt** (before quirks).
|
||||
|
||||
**Translation Frame only** (one compact line per field; evidence-based):
|
||||
- `世界観 (Theme / setting)` - genre, world type, core atmosphere
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
REM When deployed on a game: this file sits in the game root; patch scripts live in .\gameupdate\
|
||||
REM In this repo the same layout is kept for neatness: this bat is under DazedMTLTool\gameupdate\ next to a nested gameupdate\ folder with patch.ps1.
|
||||
REM In this repo the same layout is kept for neatness: this bat is under gameupdate\ next to a nested gameupdate\ folder with patch.ps1.
|
||||
set "GU_ROOT=%~dp0"
|
||||
REM Game root is this batch file's folder (not %%CD%%, so full-path launches still work).
|
||||
set "GAME_ROOT=!GU_ROOT!"
|
||||
|
|
|
|||
|
|
@ -1,159 +1,159 @@
|
|||
# Apply Patch
|
||||
1. Click Code
|
||||
2. Click Download ZIP
|
||||
3. Extract to game folder and Replace All.
|
||||
|
||||
## Future Patching
|
||||
1. Run **`GameUpdate.bat`** to auto patch (Windows).
|
||||
|
||||
### Folder layout
|
||||
|
||||
**In this translation-tool repo:** Patch payloads stay tidy inside **`gameupdate/gameupdate/`** (`patch.ps1`, `patch-config.txt`, etc.). **`GameUpdate.bat`** and **`GameUpdate_linux.sh`** sit one level up, under **`gameupdate/`**.
|
||||
|
||||
**On an installed game (what gets copied over):** Put **`GameUpdate.bat`** in the **game root**, next to the game exe. Put **`patch.ps1`** and friends inside **`gameupdate\`** under that same root (mirror names—still **`gameupdate\`**). **`GameUpdate.bat`** finds **`gameupdate\patch.ps1`** from its own folder, so **`GameRoot`** is correct even if the console cwd is somewhere else.
|
||||
|
||||
**Copy checklist:** From **`DazedMTLTool/gameupdate/`**, copy **`GameUpdate.bat`** to `<game>\`; copy everything inside **`gameupdate/gameupdate/`** into `<game>\gameupdate\`.
|
||||
|
||||
2. Optional: set `GAMEUPDATE_PROMPT_PWSH=1` before running `GameUpdate.bat` if you want users to be prompted to install PowerShell 7 via winget.
|
||||
3. Optional: set `GAMEUPDATE_DL_ATTEMPTS` (default `2`) to control retries for API checks/downloads. Lower values fail faster; higher values tolerate flaky networks.
|
||||
|
||||
### patch-config.txt
|
||||
|
||||
Create `gameupdate/patch-config.txt` next to `patch.ps1` (see `patch-config.example.txt`).
|
||||
|
||||
In DazedMTLTool, set **Config → Game Update Defaults** (forge, host, org/username, branch) once. Step 1 **Copy gameupdate/** writes those into each game's `patch-config.txt`. You still set `repo=` per game.
|
||||
|
||||
```txt
|
||||
forge=gitlab
|
||||
host=gitgud.io
|
||||
username=YOUR_ORG_OR_USER
|
||||
repo=YOUR_PATCH_REPO
|
||||
branch=main
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| `forge` | `gitlab` (default), `github`, or `forgejo` (`gitea` also accepted) |
|
||||
| `host` | Hostname only. Defaults: `gitgud.io` / `github.com` / `codeberg.org` |
|
||||
| `username` | Owner / org / group (`owner=` / `org=` aliases work) |
|
||||
| `repo` | Repository name |
|
||||
| `branch` | Branch to track |
|
||||
|
||||
Examples:
|
||||
|
||||
```txt
|
||||
# GitLab / gitgud
|
||||
forge=gitlab
|
||||
host=gitgud.io
|
||||
username=myorg
|
||||
repo=cool-game-en
|
||||
branch=main
|
||||
|
||||
# GitHub
|
||||
forge=github
|
||||
host=github.com
|
||||
username=myorg
|
||||
repo=cool-game-en
|
||||
branch=main
|
||||
|
||||
# Forgejo / Codeberg
|
||||
forge=forgejo
|
||||
host=codeberg.org
|
||||
username=myorg
|
||||
repo=cool-game-en
|
||||
branch=main
|
||||
```
|
||||
|
||||
Older configs with only `username` / `repo` / `branch` still work (assumes GitLab on `gitgud.io`).
|
||||
|
||||
# Troubleshooting
|
||||
**GAMEUPDATE.bat doesn't update and closes immediately**
|
||||
1. Make sure your path doesn't contain any Japanese characters or lots of whitespace.
|
||||
2. Make sure you actually have permissions in the folder
|
||||
3. Auto-update calls the forge's public HTTP API (GitLab `/api/v4`, GitHub `/repos/...`, Forgejo `/api/v1`), not the web “Download ZIP” button - no account is required for public patch repos.
|
||||
|
||||
# Wolf Games
|
||||
WOLF RPG installs from DLSite ship a master `Data.wolf` archive that takes priority over loose English patch files. `GameUpdate.bat` detects that and unpacks it automatically with the bundled `UberWolfCli.exe` (MIT, [Sinflower/UberWolf](https://github.com/Sinflower/UberWolf)), then renames `Data.wolf` to `Data.wolf.bak` so the patched loose `Data/` files load.
|
||||
|
||||
Just run **`GameUpdate.bat`**. No manual UberWolf download is required.
|
||||
|
||||
If unpack fails (rare Pro / protected builds), unpack once with [UberWolf](https://github.com/Sinflower/UberWolf/releases), ensure a loose `Data/` folder exists, rename or remove `Data.wolf`, delete `gameupdate/previous_patch_sha.txt`, and run `GameUpdate.bat` again.
|
||||
|
||||
# Edit/Contribute
|
||||
TLDR 3 steps.
|
||||
|
||||
Fork the repository.
|
||||
Make the changes.
|
||||
Submit a merge request.
|
||||
|
||||
If everything looks good and doesn't break things I'll merge it in.
|
||||
|
||||
Longer Version:
|
||||
|
||||
# Required Software:
|
||||
* [VSCode](https://code.visualstudio.com/) Make sure you check all the boxes for context menus.
|
||||
* [Git](https://git-scm.com/downloads) (Use the default for everything. Just keep clicking Next)
|
||||
|
||||
# Guide to contributing
|
||||
|
||||
### 1. Fork the Repository
|
||||
- Go to the repository you want to fork.
|
||||
- Click the "Fork" button.
|
||||
|
||||
### 2. Clone Your Fork
|
||||
- Clone your forked repository to your local machine.
|
||||
```sh
|
||||
git clone https://gitgud.io/YOUR_USERNAME/REPO_NAME.git
|
||||
```
|
||||
|
||||
### 3. Make Your Changes (In VSCode)
|
||||
- Edit the files locally on your new branch using VSCode.
|
||||
- Add and commit your changes.
|
||||
```sh
|
||||
git add .
|
||||
git commit -m "Description of your changes"
|
||||
```
|
||||
|
||||
### 4. Push Your Changes
|
||||
- Push your changes to your fork on GitGud.io.
|
||||
```sh
|
||||
git push origin your-feature-branch
|
||||
```
|
||||
|
||||
### 5. Create a Merge Request
|
||||
- Go to your fork on GitGud.io.
|
||||
- Click on "Merge Requests" in the sidebar.
|
||||
- Click the "New merge request" button.
|
||||
- Select the branch you made changes to and the target project (the original repo).
|
||||
- Provide a title and description for your merge request and submit it.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Assuming you want to fork a repository named `example-project`:
|
||||
|
||||
### 1. Fork the Repo
|
||||
- Navigate to `https://gitgud.io/original_user/example-project` and click "Fork".
|
||||
|
||||
### 2. Clone Your Fork
|
||||
```sh
|
||||
git clone https://gitgud.io/YOUR_USERNAME/example-project.git
|
||||
```
|
||||
|
||||
### 3. Make Changes and Commit
|
||||
```sh
|
||||
# Make changes to the files
|
||||
git add .
|
||||
git commit -m "Add new feature to example project"
|
||||
```
|
||||
|
||||
### 4. Push Changes
|
||||
```sh
|
||||
git push origin add-new-feature
|
||||
```
|
||||
|
||||
### 5. Create a Merge Request
|
||||
- Go to `https://gitgud.io/YOUR_USERNAME/example-project/merge_requests` and click on "New merge request"
|
||||
- Choose the source branch `add-new-feature` and target branch (default: `main` or `master`)
|
||||
# Apply Patch
|
||||
1. Click Code
|
||||
2. Click Download ZIP
|
||||
3. Extract to game folder and Replace All.
|
||||
|
||||
## Future Patching
|
||||
1. Run **`GameUpdate.bat`** to auto patch (Windows).
|
||||
|
||||
### Folder layout
|
||||
|
||||
**In this translation-tool repo:** Patch payloads stay tidy inside **`gameupdate/gameupdate/`** (`patch.ps1`, `patch-config.txt`, etc.). **`GameUpdate.bat`** and **`GameUpdate_linux.sh`** sit one level up, under **`gameupdate/`**.
|
||||
|
||||
**On an installed game (what gets copied over):** Put **`GameUpdate.bat`** in the **game root**, next to the game exe. Put **`patch.ps1`** and friends inside **`gameupdate\`** under that same root (mirror names—still **`gameupdate\`**). **`GameUpdate.bat`** finds **`gameupdate\patch.ps1`** from its own folder, so **`GameRoot`** is correct even if the console cwd is somewhere else.
|
||||
|
||||
**Copy checklist:** From the tool repo's **`gameupdate/`**, copy **`GameUpdate.bat`** to `<game>\`; copy everything inside **`gameupdate/gameupdate/`** into `<game>\gameupdate\`.
|
||||
|
||||
2. Optional: set `GAMEUPDATE_PROMPT_PWSH=1` before running `GameUpdate.bat` if you want users to be prompted to install PowerShell 7 via winget.
|
||||
3. Optional: set `GAMEUPDATE_DL_ATTEMPTS` (default `2`) to control retries for API checks/downloads. Lower values fail faster; higher values tolerate flaky networks.
|
||||
|
||||
### patch-config.txt
|
||||
|
||||
Create `gameupdate/patch-config.txt` next to `patch.ps1` (see `patch-config.example.txt`).
|
||||
|
||||
In DazedTL, set **Config → Game Update Defaults** (forge, host, org/username, branch) once. Step 1 **Copy gameupdate/** writes those into each game's `patch-config.txt`. You still set `repo=` per game.
|
||||
|
||||
```txt
|
||||
forge=gitlab
|
||||
host=gitgud.io
|
||||
username=YOUR_ORG_OR_USER
|
||||
repo=YOUR_PATCH_REPO
|
||||
branch=main
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| `forge` | `gitlab` (default), `github`, or `forgejo` (`gitea` also accepted) |
|
||||
| `host` | Hostname only. Defaults: `gitgud.io` / `github.com` / `codeberg.org` |
|
||||
| `username` | Owner / org / group (`owner=` / `org=` aliases work) |
|
||||
| `repo` | Repository name |
|
||||
| `branch` | Branch to track |
|
||||
|
||||
Examples:
|
||||
|
||||
```txt
|
||||
# GitLab / gitgud
|
||||
forge=gitlab
|
||||
host=gitgud.io
|
||||
username=myorg
|
||||
repo=cool-game-en
|
||||
branch=main
|
||||
|
||||
# GitHub
|
||||
forge=github
|
||||
host=github.com
|
||||
username=myorg
|
||||
repo=cool-game-en
|
||||
branch=main
|
||||
|
||||
# Forgejo / Codeberg
|
||||
forge=forgejo
|
||||
host=codeberg.org
|
||||
username=myorg
|
||||
repo=cool-game-en
|
||||
branch=main
|
||||
```
|
||||
|
||||
Older configs with only `username` / `repo` / `branch` still work (assumes GitLab on `gitgud.io`).
|
||||
|
||||
# Troubleshooting
|
||||
**GAMEUPDATE.bat doesn't update and closes immediately**
|
||||
1. Make sure your path doesn't contain any Japanese characters or lots of whitespace.
|
||||
2. Make sure you actually have permissions in the folder
|
||||
3. Auto-update calls the forge's public HTTP API (GitLab `/api/v4`, GitHub `/repos/...`, Forgejo `/api/v1`), not the web “Download ZIP” button - no account is required for public patch repos.
|
||||
|
||||
# Wolf Games
|
||||
WOLF RPG installs from DLSite ship a master `Data.wolf` archive that takes priority over loose English patch files. `GameUpdate.bat` detects that and unpacks it automatically with the bundled `UberWolfCli.exe` (MIT, [Sinflower/UberWolf](https://github.com/Sinflower/UberWolf)), then renames `Data.wolf` to `Data.wolf.bak` so the patched loose `Data/` files load.
|
||||
|
||||
Just run **`GameUpdate.bat`**. No manual UberWolf download is required.
|
||||
|
||||
If unpack fails (rare Pro / protected builds), unpack once with [UberWolf](https://github.com/Sinflower/UberWolf/releases), ensure a loose `Data/` folder exists, rename or remove `Data.wolf`, delete `gameupdate/previous_patch_sha.txt`, and run `GameUpdate.bat` again.
|
||||
|
||||
# Edit/Contribute
|
||||
TLDR 3 steps.
|
||||
|
||||
Fork the repository.
|
||||
Make the changes.
|
||||
Submit a merge request.
|
||||
|
||||
If everything looks good and doesn't break things I'll merge it in.
|
||||
|
||||
Longer Version:
|
||||
|
||||
# Required Software:
|
||||
* [VSCode](https://code.visualstudio.com/) Make sure you check all the boxes for context menus.
|
||||
* [Git](https://git-scm.com/downloads) (Use the default for everything. Just keep clicking Next)
|
||||
|
||||
# Guide to contributing
|
||||
|
||||
### 1. Fork the Repository
|
||||
- Go to the repository you want to fork.
|
||||
- Click the "Fork" button.
|
||||
|
||||
### 2. Clone Your Fork
|
||||
- Clone your forked repository to your local machine.
|
||||
```sh
|
||||
git clone https://gitgud.io/YOUR_USERNAME/REPO_NAME.git
|
||||
```
|
||||
|
||||
### 3. Make Your Changes (In VSCode)
|
||||
- Edit the files locally on your new branch using VSCode.
|
||||
- Add and commit your changes.
|
||||
```sh
|
||||
git add .
|
||||
git commit -m "Description of your changes"
|
||||
```
|
||||
|
||||
### 4. Push Your Changes
|
||||
- Push your changes to your fork on GitGud.io.
|
||||
```sh
|
||||
git push origin your-feature-branch
|
||||
```
|
||||
|
||||
### 5. Create a Merge Request
|
||||
- Go to your fork on GitGud.io.
|
||||
- Click on "Merge Requests" in the sidebar.
|
||||
- Click the "New merge request" button.
|
||||
- Select the branch you made changes to and the target project (the original repo).
|
||||
- Provide a title and description for your merge request and submit it.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Assuming you want to fork a repository named `example-project`:
|
||||
|
||||
### 1. Fork the Repo
|
||||
- Navigate to `https://gitgud.io/original_user/example-project` and click "Fork".
|
||||
|
||||
### 2. Clone Your Fork
|
||||
```sh
|
||||
git clone https://gitgud.io/YOUR_USERNAME/example-project.git
|
||||
```
|
||||
|
||||
### 3. Make Changes and Commit
|
||||
```sh
|
||||
# Make changes to the files
|
||||
git add .
|
||||
git commit -m "Add new feature to example project"
|
||||
```
|
||||
|
||||
### 4. Push Changes
|
||||
```sh
|
||||
git push origin add-new-feature
|
||||
```
|
||||
|
||||
### 5. Create a Merge Request
|
||||
- Go to `https://gitgud.io/YOUR_USERNAME/example-project/merge_requests` and click on "New merge request"
|
||||
- Choose the source branch `add-new-feature` and target branch (default: `main` or `master`)
|
||||
- Fill in the details and submit the merge request
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# GameUpdate patch source (copy to patch-config.txt and edit).
|
||||
# Lines starting with # are comments. Keys are case-insensitive.
|
||||
#
|
||||
# Tip: set forge / host / username / branch once in DazedMTLTool
|
||||
# Tip: set forge / host / username / branch once in DazedTL
|
||||
# Config → Game Update Defaults. Step 1 "Copy gameupdate/" writes
|
||||
# those into this file automatically (you still set repo= per game).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
"""
|
||||
DazedMTLTool GUI Package
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "DazedTranslations"
|
||||
|
||||
# Package imports
|
||||
from .main import DazedMTLGUI
|
||||
from .config_tab import ConfigTab
|
||||
from .rpgmaker_tab import RPGMakerTab
|
||||
from .log_viewer import LogViewer
|
||||
from .file_manager import FileManager
|
||||
from .workflow_tab import WorkflowTab
|
||||
|
||||
__all__ = [
|
||||
"DazedMTLGUI",
|
||||
"ConfigTab",
|
||||
"RPGMakerTab",
|
||||
"LogViewer",
|
||||
"FileManager",
|
||||
"WorkflowTab",
|
||||
]
|
||||
"""
|
||||
DazedTL GUI Package
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "DazedTranslations"
|
||||
|
||||
# Package imports
|
||||
from .main import DazedMTLGUI
|
||||
from .config_tab import ConfigTab
|
||||
from .rpgmaker_tab import RPGMakerTab
|
||||
from .log_viewer import LogViewer
|
||||
from .file_manager import FileManager
|
||||
from .workflow_tab import WorkflowTab
|
||||
|
||||
__all__ = [
|
||||
"DazedMTLGUI",
|
||||
"ConfigTab",
|
||||
"RPGMakerTab",
|
||||
"LogViewer",
|
||||
"FileManager",
|
||||
"WorkflowTab",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,426 +1,428 @@
|
|||
"""
|
||||
File Manager - Handle input and output files
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QPushButton, QLabel, QFileDialog, QMessageBox, QGroupBox,
|
||||
QSplitter, QTextEdit, QProgressBar, QCheckBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
|
||||
import json
|
||||
|
||||
|
||||
class FileManager(QWidget):
|
||||
"""Widget for managing input and output files."""
|
||||
|
||||
files_changed = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Persistent settings for remembering last-open directory
|
||||
try:
|
||||
self.settings = QSettings("DazedTranslations", "DazedMTLTool")
|
||||
except Exception:
|
||||
self.settings = None
|
||||
self.input_files = []
|
||||
self.output_directory = Path("translated")
|
||||
self.init_ui()
|
||||
self.refresh_files()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialize the user interface."""
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Create splitter for input and output sections
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Input files section
|
||||
input_group = self.create_input_section()
|
||||
splitter.addWidget(input_group)
|
||||
|
||||
# Output files section
|
||||
output_group = self.create_output_section()
|
||||
splitter.addWidget(output_group)
|
||||
|
||||
splitter.setSizes([400, 400])
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# Progress section
|
||||
progress_group = self.create_progress_section()
|
||||
layout.addWidget(progress_group)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_input_section(self):
|
||||
"""Create the input files section."""
|
||||
group = QGroupBox("Input Files")
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.add_files_button = QPushButton("Add Files")
|
||||
self.add_files_button.clicked.connect(self.add_input_files)
|
||||
|
||||
self.add_folder_button = QPushButton("Add Folder")
|
||||
self.add_folder_button.clicked.connect(self.add_input_folder)
|
||||
|
||||
self.remove_files_button = QPushButton("Remove Selected")
|
||||
self.remove_files_button.clicked.connect(self.remove_selected_files)
|
||||
|
||||
self.clear_files_button = QPushButton("Clear All")
|
||||
self.clear_files_button.clicked.connect(self.clear_input_files)
|
||||
|
||||
button_layout.addWidget(self.add_files_button)
|
||||
button_layout.addWidget(self.add_folder_button)
|
||||
button_layout.addWidget(self.remove_files_button)
|
||||
button_layout.addWidget(self.clear_files_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# File tree
|
||||
self.input_tree = QTreeWidget()
|
||||
self.input_tree.setHeaderLabels(["File", "Size", "Type"])
|
||||
self.input_tree.itemSelectionChanged.connect(self.on_input_selection_changed)
|
||||
layout.addWidget(self.input_tree)
|
||||
|
||||
# File info
|
||||
self.input_info = QTextEdit()
|
||||
self.input_info.setMaximumHeight(100)
|
||||
self.input_info.setReadOnly(True)
|
||||
layout.addWidget(self.input_info)
|
||||
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def create_output_section(self):
|
||||
"""Create the output files section."""
|
||||
group = QGroupBox("Output Files")
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Output directory selection
|
||||
dir_layout = QHBoxLayout()
|
||||
|
||||
self.output_dir_label = QLabel(str(self.output_directory))
|
||||
self.output_dir_label.setStyleSheet("border: 1px solid #555; padding: 5px;")
|
||||
|
||||
self.browse_output_button = QPushButton("Browse")
|
||||
self.browse_output_button.clicked.connect(self.browse_output_directory)
|
||||
|
||||
dir_layout.addWidget(QLabel("Output Directory:"))
|
||||
dir_layout.addWidget(self.output_dir_label)
|
||||
dir_layout.addWidget(self.browse_output_button)
|
||||
|
||||
layout.addLayout(dir_layout)
|
||||
|
||||
# Output file tree
|
||||
self.output_tree = QTreeWidget()
|
||||
self.output_tree.setHeaderLabels(["File", "Size", "Modified"])
|
||||
self.output_tree.itemSelectionChanged.connect(self.on_output_selection_changed)
|
||||
layout.addWidget(self.output_tree)
|
||||
|
||||
# Output file preview
|
||||
self.output_preview = QTextEdit()
|
||||
self.output_preview.setMaximumHeight(100)
|
||||
self.output_preview.setReadOnly(True)
|
||||
layout.addWidget(self.output_preview)
|
||||
|
||||
# Output buttons
|
||||
output_button_layout = QHBoxLayout()
|
||||
|
||||
self.open_output_button = QPushButton("Open in Editor")
|
||||
self.open_output_button.clicked.connect(self.open_output_file)
|
||||
|
||||
self.delete_output_button = QPushButton("Delete Selected")
|
||||
self.delete_output_button.clicked.connect(self.delete_output_file)
|
||||
|
||||
output_button_layout.addWidget(self.open_output_button)
|
||||
output_button_layout.addWidget(self.delete_output_button)
|
||||
output_button_layout.addStretch()
|
||||
|
||||
layout.addLayout(output_button_layout)
|
||||
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def create_progress_section(self):
|
||||
"""Create the progress monitoring section."""
|
||||
group = QGroupBox("Translation Progress")
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Progress bar
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# Status and options
|
||||
status_layout = QHBoxLayout()
|
||||
|
||||
self.status_label = QLabel("Ready")
|
||||
self.backup_cb = QCheckBox("Create Backups")
|
||||
self.backup_cb.setChecked(True)
|
||||
|
||||
status_layout.addWidget(self.status_label)
|
||||
status_layout.addStretch()
|
||||
status_layout.addWidget(self.backup_cb)
|
||||
|
||||
layout.addLayout(status_layout)
|
||||
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def add_input_files(self):
|
||||
"""Add input files via dialog."""
|
||||
# Try to restore last used directory
|
||||
start_dir = ""
|
||||
try:
|
||||
if self.settings:
|
||||
start_dir = self.settings.value("last_open_dir", "") or ""
|
||||
except Exception:
|
||||
start_dir = ""
|
||||
|
||||
files, _ = QFileDialog.getOpenFileNames(
|
||||
self,
|
||||
"Select Input Files",
|
||||
start_dir,
|
||||
"JSON Files (*.json);;All Files (*)"
|
||||
)
|
||||
|
||||
for file_path in files:
|
||||
if file_path not in self.input_files:
|
||||
self.input_files.append(file_path)
|
||||
|
||||
# Persist directory used
|
||||
try:
|
||||
if self.settings and len(files) > 0:
|
||||
import os
|
||||
self.settings.setValue("last_open_dir", os.path.dirname(files[0]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.refresh_input_files()
|
||||
|
||||
def add_input_folder(self):
|
||||
"""Add all files from a folder."""
|
||||
folder = QFileDialog.getExistingDirectory(self, "Select Input Folder")
|
||||
|
||||
if folder:
|
||||
folder_path = Path(folder)
|
||||
for file_path in folder_path.rglob("*.json"):
|
||||
file_str = str(file_path)
|
||||
if file_str not in self.input_files:
|
||||
self.input_files.append(file_str)
|
||||
|
||||
self.refresh_input_files()
|
||||
|
||||
def remove_selected_files(self):
|
||||
"""Remove selected input files."""
|
||||
selected_items = self.input_tree.selectedItems()
|
||||
for item in selected_items:
|
||||
file_path = item.text(0)
|
||||
if file_path in self.input_files:
|
||||
self.input_files.remove(file_path)
|
||||
|
||||
self.refresh_input_files()
|
||||
|
||||
def clear_input_files(self):
|
||||
"""Clear all input files."""
|
||||
self.input_files.clear()
|
||||
self.refresh_input_files()
|
||||
|
||||
def refresh_input_files(self):
|
||||
"""Refresh the input files display."""
|
||||
self.input_tree.clear()
|
||||
|
||||
for file_path in self.input_files:
|
||||
path_obj = Path(file_path)
|
||||
if path_obj.exists():
|
||||
size = path_obj.stat().st_size
|
||||
file_type = path_obj.suffix
|
||||
|
||||
item = QTreeWidgetItem([
|
||||
path_obj.name,
|
||||
f"{size:,} bytes",
|
||||
file_type
|
||||
])
|
||||
item.setToolTip(0, str(path_obj))
|
||||
self.input_tree.addTopLevelItem(item)
|
||||
else:
|
||||
# File doesn't exist - show in red
|
||||
item = QTreeWidgetItem([
|
||||
f"{path_obj.name} (Missing)",
|
||||
"0 bytes",
|
||||
"N/A"
|
||||
])
|
||||
item.setBackground(0, Qt.red)
|
||||
self.input_tree.addTopLevelItem(item)
|
||||
|
||||
# Update info
|
||||
self.update_input_info()
|
||||
|
||||
def refresh_output_files(self):
|
||||
"""Refresh the output files display."""
|
||||
self.output_tree.clear()
|
||||
|
||||
if self.output_directory.exists():
|
||||
for file_path in self.output_directory.iterdir():
|
||||
if file_path.is_file():
|
||||
size = file_path.stat().st_size
|
||||
modified = file_path.stat().st_mtime
|
||||
|
||||
import datetime
|
||||
modified_str = datetime.datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
item = QTreeWidgetItem([
|
||||
file_path.name,
|
||||
f"{size:,} bytes",
|
||||
modified_str
|
||||
])
|
||||
item.setToolTip(0, str(file_path))
|
||||
self.output_tree.addTopLevelItem(item)
|
||||
|
||||
def browse_output_directory(self):
|
||||
"""Browse for output directory."""
|
||||
directory = QFileDialog.getExistingDirectory(
|
||||
self, "Select Output Directory", str(self.output_directory)
|
||||
)
|
||||
|
||||
if directory:
|
||||
self.output_directory = Path(directory)
|
||||
self.output_dir_label.setText(str(self.output_directory))
|
||||
self.refresh_output_files()
|
||||
|
||||
def refresh_files(self):
|
||||
"""Refresh both input and output file displays."""
|
||||
self.refresh_input_files()
|
||||
self.refresh_output_files()
|
||||
|
||||
def on_input_selection_changed(self):
|
||||
"""Handle input file selection change."""
|
||||
selected_items = self.input_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
|
||||
# Find the full path
|
||||
full_path = None
|
||||
for file_path in self.input_files:
|
||||
if Path(file_path).name == file_name:
|
||||
full_path = file_path
|
||||
break
|
||||
|
||||
if full_path and Path(full_path).exists():
|
||||
try:
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Show preview of file structure
|
||||
if full_path.endswith('.json'):
|
||||
data = json.loads(content)
|
||||
if isinstance(data, dict):
|
||||
keys = list(data.keys())[:10] # Show first 10 keys
|
||||
preview = f"JSON Object with {len(data)} keys:\n"
|
||||
preview += "\n".join(keys)
|
||||
if len(data) > 10:
|
||||
preview += f"\n... and {len(data) - 10} more"
|
||||
elif isinstance(data, list):
|
||||
preview = f"JSON Array with {len(data)} items"
|
||||
else:
|
||||
preview = f"JSON: {type(data).__name__}"
|
||||
else:
|
||||
preview = content[:500] + "..." if len(content) > 500 else content
|
||||
|
||||
self.input_info.setPlainText(preview)
|
||||
except Exception as e:
|
||||
self.input_info.setPlainText(f"Error reading file: {str(e)}")
|
||||
|
||||
def on_output_selection_changed(self):
|
||||
"""Handle output file selection change."""
|
||||
selected_items = self.output_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
file_path = self.output_directory / file_name
|
||||
|
||||
if file_path.exists():
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
preview = content[:500] + "..." if len(content) > 500 else content
|
||||
self.output_preview.setPlainText(preview)
|
||||
except Exception as e:
|
||||
self.output_preview.setPlainText(f"Error reading file: {str(e)}")
|
||||
|
||||
def open_output_file(self):
|
||||
"""Open selected output file in external editor."""
|
||||
selected_items = self.output_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
file_path = self.output_directory / file_name
|
||||
|
||||
if file_path.exists():
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(['notepad.exe', str(file_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
QMessageBox.warning(self, "Warning", "Could not open file in external editor")
|
||||
|
||||
def delete_output_file(self):
|
||||
"""Delete selected output file."""
|
||||
selected_items = self.output_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
file_path = self.output_directory / file_name
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm Deletion",
|
||||
f"Are you sure you want to delete '{file_name}'?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes and file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
self.refresh_output_files()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}")
|
||||
|
||||
def update_input_info(self):
|
||||
"""Update the input files information."""
|
||||
total_files = len(self.input_files)
|
||||
total_size = 0
|
||||
|
||||
for file_path in self.input_files:
|
||||
path_obj = Path(file_path)
|
||||
if path_obj.exists():
|
||||
total_size += path_obj.stat().st_size
|
||||
|
||||
info = f"Total Files: {total_files}\nTotal Size: {total_size:,} bytes"
|
||||
|
||||
# Count by file type
|
||||
extensions = {}
|
||||
for file_path in self.input_files:
|
||||
ext = Path(file_path).suffix
|
||||
extensions[ext] = extensions.get(ext, 0) + 1
|
||||
|
||||
if extensions:
|
||||
info += "\n\nFile Types:"
|
||||
for ext, count in sorted(extensions.items()):
|
||||
info += f"\n{ext or 'No extension'}: {count}"
|
||||
|
||||
self.input_info.setPlainText(info)
|
||||
|
||||
def get_input_files(self):
|
||||
"""Get list of input files."""
|
||||
return self.input_files.copy()
|
||||
|
||||
def get_output_directory(self):
|
||||
"""Get output directory path."""
|
||||
return self.output_directory
|
||||
"""
|
||||
File Manager - Handle input and output files
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QPushButton, QLabel, QFileDialog, QMessageBox, QGroupBox,
|
||||
QSplitter, QTextEdit, QProgressBar, QCheckBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
|
||||
import json
|
||||
|
||||
from util.paths import APP_NAME, ORG_NAME
|
||||
|
||||
|
||||
class FileManager(QWidget):
|
||||
"""Widget for managing input and output files."""
|
||||
|
||||
files_changed = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Persistent settings for remembering last-open directory
|
||||
try:
|
||||
self.settings = QSettings(ORG_NAME, APP_NAME)
|
||||
except Exception:
|
||||
self.settings = None
|
||||
self.input_files = []
|
||||
self.output_directory = Path("translated")
|
||||
self.init_ui()
|
||||
self.refresh_files()
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialize the user interface."""
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Create splitter for input and output sections
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
# Input files section
|
||||
input_group = self.create_input_section()
|
||||
splitter.addWidget(input_group)
|
||||
|
||||
# Output files section
|
||||
output_group = self.create_output_section()
|
||||
splitter.addWidget(output_group)
|
||||
|
||||
splitter.setSizes([400, 400])
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# Progress section
|
||||
progress_group = self.create_progress_section()
|
||||
layout.addWidget(progress_group)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_input_section(self):
|
||||
"""Create the input files section."""
|
||||
group = QGroupBox("Input Files")
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.add_files_button = QPushButton("Add Files")
|
||||
self.add_files_button.clicked.connect(self.add_input_files)
|
||||
|
||||
self.add_folder_button = QPushButton("Add Folder")
|
||||
self.add_folder_button.clicked.connect(self.add_input_folder)
|
||||
|
||||
self.remove_files_button = QPushButton("Remove Selected")
|
||||
self.remove_files_button.clicked.connect(self.remove_selected_files)
|
||||
|
||||
self.clear_files_button = QPushButton("Clear All")
|
||||
self.clear_files_button.clicked.connect(self.clear_input_files)
|
||||
|
||||
button_layout.addWidget(self.add_files_button)
|
||||
button_layout.addWidget(self.add_folder_button)
|
||||
button_layout.addWidget(self.remove_files_button)
|
||||
button_layout.addWidget(self.clear_files_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# File tree
|
||||
self.input_tree = QTreeWidget()
|
||||
self.input_tree.setHeaderLabels(["File", "Size", "Type"])
|
||||
self.input_tree.itemSelectionChanged.connect(self.on_input_selection_changed)
|
||||
layout.addWidget(self.input_tree)
|
||||
|
||||
# File info
|
||||
self.input_info = QTextEdit()
|
||||
self.input_info.setMaximumHeight(100)
|
||||
self.input_info.setReadOnly(True)
|
||||
layout.addWidget(self.input_info)
|
||||
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def create_output_section(self):
|
||||
"""Create the output files section."""
|
||||
group = QGroupBox("Output Files")
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Output directory selection
|
||||
dir_layout = QHBoxLayout()
|
||||
|
||||
self.output_dir_label = QLabel(str(self.output_directory))
|
||||
self.output_dir_label.setStyleSheet("border: 1px solid #555; padding: 5px;")
|
||||
|
||||
self.browse_output_button = QPushButton("Browse")
|
||||
self.browse_output_button.clicked.connect(self.browse_output_directory)
|
||||
|
||||
dir_layout.addWidget(QLabel("Output Directory:"))
|
||||
dir_layout.addWidget(self.output_dir_label)
|
||||
dir_layout.addWidget(self.browse_output_button)
|
||||
|
||||
layout.addLayout(dir_layout)
|
||||
|
||||
# Output file tree
|
||||
self.output_tree = QTreeWidget()
|
||||
self.output_tree.setHeaderLabels(["File", "Size", "Modified"])
|
||||
self.output_tree.itemSelectionChanged.connect(self.on_output_selection_changed)
|
||||
layout.addWidget(self.output_tree)
|
||||
|
||||
# Output file preview
|
||||
self.output_preview = QTextEdit()
|
||||
self.output_preview.setMaximumHeight(100)
|
||||
self.output_preview.setReadOnly(True)
|
||||
layout.addWidget(self.output_preview)
|
||||
|
||||
# Output buttons
|
||||
output_button_layout = QHBoxLayout()
|
||||
|
||||
self.open_output_button = QPushButton("Open in Editor")
|
||||
self.open_output_button.clicked.connect(self.open_output_file)
|
||||
|
||||
self.delete_output_button = QPushButton("Delete Selected")
|
||||
self.delete_output_button.clicked.connect(self.delete_output_file)
|
||||
|
||||
output_button_layout.addWidget(self.open_output_button)
|
||||
output_button_layout.addWidget(self.delete_output_button)
|
||||
output_button_layout.addStretch()
|
||||
|
||||
layout.addLayout(output_button_layout)
|
||||
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def create_progress_section(self):
|
||||
"""Create the progress monitoring section."""
|
||||
group = QGroupBox("Translation Progress")
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Progress bar
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# Status and options
|
||||
status_layout = QHBoxLayout()
|
||||
|
||||
self.status_label = QLabel("Ready")
|
||||
self.backup_cb = QCheckBox("Create Backups")
|
||||
self.backup_cb.setChecked(True)
|
||||
|
||||
status_layout.addWidget(self.status_label)
|
||||
status_layout.addStretch()
|
||||
status_layout.addWidget(self.backup_cb)
|
||||
|
||||
layout.addLayout(status_layout)
|
||||
|
||||
group.setLayout(layout)
|
||||
return group
|
||||
|
||||
def add_input_files(self):
|
||||
"""Add input files via dialog."""
|
||||
# Try to restore last used directory
|
||||
start_dir = ""
|
||||
try:
|
||||
if self.settings:
|
||||
start_dir = self.settings.value("last_open_dir", "") or ""
|
||||
except Exception:
|
||||
start_dir = ""
|
||||
|
||||
files, _ = QFileDialog.getOpenFileNames(
|
||||
self,
|
||||
"Select Input Files",
|
||||
start_dir,
|
||||
"JSON Files (*.json);;All Files (*)"
|
||||
)
|
||||
|
||||
for file_path in files:
|
||||
if file_path not in self.input_files:
|
||||
self.input_files.append(file_path)
|
||||
|
||||
# Persist directory used
|
||||
try:
|
||||
if self.settings and len(files) > 0:
|
||||
import os
|
||||
self.settings.setValue("last_open_dir", os.path.dirname(files[0]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.refresh_input_files()
|
||||
|
||||
def add_input_folder(self):
|
||||
"""Add all files from a folder."""
|
||||
folder = QFileDialog.getExistingDirectory(self, "Select Input Folder")
|
||||
|
||||
if folder:
|
||||
folder_path = Path(folder)
|
||||
for file_path in folder_path.rglob("*.json"):
|
||||
file_str = str(file_path)
|
||||
if file_str not in self.input_files:
|
||||
self.input_files.append(file_str)
|
||||
|
||||
self.refresh_input_files()
|
||||
|
||||
def remove_selected_files(self):
|
||||
"""Remove selected input files."""
|
||||
selected_items = self.input_tree.selectedItems()
|
||||
for item in selected_items:
|
||||
file_path = item.text(0)
|
||||
if file_path in self.input_files:
|
||||
self.input_files.remove(file_path)
|
||||
|
||||
self.refresh_input_files()
|
||||
|
||||
def clear_input_files(self):
|
||||
"""Clear all input files."""
|
||||
self.input_files.clear()
|
||||
self.refresh_input_files()
|
||||
|
||||
def refresh_input_files(self):
|
||||
"""Refresh the input files display."""
|
||||
self.input_tree.clear()
|
||||
|
||||
for file_path in self.input_files:
|
||||
path_obj = Path(file_path)
|
||||
if path_obj.exists():
|
||||
size = path_obj.stat().st_size
|
||||
file_type = path_obj.suffix
|
||||
|
||||
item = QTreeWidgetItem([
|
||||
path_obj.name,
|
||||
f"{size:,} bytes",
|
||||
file_type
|
||||
])
|
||||
item.setToolTip(0, str(path_obj))
|
||||
self.input_tree.addTopLevelItem(item)
|
||||
else:
|
||||
# File doesn't exist - show in red
|
||||
item = QTreeWidgetItem([
|
||||
f"{path_obj.name} (Missing)",
|
||||
"0 bytes",
|
||||
"N/A"
|
||||
])
|
||||
item.setBackground(0, Qt.red)
|
||||
self.input_tree.addTopLevelItem(item)
|
||||
|
||||
# Update info
|
||||
self.update_input_info()
|
||||
|
||||
def refresh_output_files(self):
|
||||
"""Refresh the output files display."""
|
||||
self.output_tree.clear()
|
||||
|
||||
if self.output_directory.exists():
|
||||
for file_path in self.output_directory.iterdir():
|
||||
if file_path.is_file():
|
||||
size = file_path.stat().st_size
|
||||
modified = file_path.stat().st_mtime
|
||||
|
||||
import datetime
|
||||
modified_str = datetime.datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
item = QTreeWidgetItem([
|
||||
file_path.name,
|
||||
f"{size:,} bytes",
|
||||
modified_str
|
||||
])
|
||||
item.setToolTip(0, str(file_path))
|
||||
self.output_tree.addTopLevelItem(item)
|
||||
|
||||
def browse_output_directory(self):
|
||||
"""Browse for output directory."""
|
||||
directory = QFileDialog.getExistingDirectory(
|
||||
self, "Select Output Directory", str(self.output_directory)
|
||||
)
|
||||
|
||||
if directory:
|
||||
self.output_directory = Path(directory)
|
||||
self.output_dir_label.setText(str(self.output_directory))
|
||||
self.refresh_output_files()
|
||||
|
||||
def refresh_files(self):
|
||||
"""Refresh both input and output file displays."""
|
||||
self.refresh_input_files()
|
||||
self.refresh_output_files()
|
||||
|
||||
def on_input_selection_changed(self):
|
||||
"""Handle input file selection change."""
|
||||
selected_items = self.input_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
|
||||
# Find the full path
|
||||
full_path = None
|
||||
for file_path in self.input_files:
|
||||
if Path(file_path).name == file_name:
|
||||
full_path = file_path
|
||||
break
|
||||
|
||||
if full_path and Path(full_path).exists():
|
||||
try:
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Show preview of file structure
|
||||
if full_path.endswith('.json'):
|
||||
data = json.loads(content)
|
||||
if isinstance(data, dict):
|
||||
keys = list(data.keys())[:10] # Show first 10 keys
|
||||
preview = f"JSON Object with {len(data)} keys:\n"
|
||||
preview += "\n".join(keys)
|
||||
if len(data) > 10:
|
||||
preview += f"\n... and {len(data) - 10} more"
|
||||
elif isinstance(data, list):
|
||||
preview = f"JSON Array with {len(data)} items"
|
||||
else:
|
||||
preview = f"JSON: {type(data).__name__}"
|
||||
else:
|
||||
preview = content[:500] + "..." if len(content) > 500 else content
|
||||
|
||||
self.input_info.setPlainText(preview)
|
||||
except Exception as e:
|
||||
self.input_info.setPlainText(f"Error reading file: {str(e)}")
|
||||
|
||||
def on_output_selection_changed(self):
|
||||
"""Handle output file selection change."""
|
||||
selected_items = self.output_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
file_path = self.output_directory / file_name
|
||||
|
||||
if file_path.exists():
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
preview = content[:500] + "..." if len(content) > 500 else content
|
||||
self.output_preview.setPlainText(preview)
|
||||
except Exception as e:
|
||||
self.output_preview.setPlainText(f"Error reading file: {str(e)}")
|
||||
|
||||
def open_output_file(self):
|
||||
"""Open selected output file in external editor."""
|
||||
selected_items = self.output_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
file_path = self.output_directory / file_name
|
||||
|
||||
if file_path.exists():
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(['notepad.exe', str(file_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
QMessageBox.warning(self, "Warning", "Could not open file in external editor")
|
||||
|
||||
def delete_output_file(self):
|
||||
"""Delete selected output file."""
|
||||
selected_items = self.output_tree.selectedItems()
|
||||
if selected_items:
|
||||
item = selected_items[0]
|
||||
file_name = item.text(0)
|
||||
file_path = self.output_directory / file_name
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm Deletion",
|
||||
f"Are you sure you want to delete '{file_name}'?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes and file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
self.refresh_output_files()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}")
|
||||
|
||||
def update_input_info(self):
|
||||
"""Update the input files information."""
|
||||
total_files = len(self.input_files)
|
||||
total_size = 0
|
||||
|
||||
for file_path in self.input_files:
|
||||
path_obj = Path(file_path)
|
||||
if path_obj.exists():
|
||||
total_size += path_obj.stat().st_size
|
||||
|
||||
info = f"Total Files: {total_files}\nTotal Size: {total_size:,} bytes"
|
||||
|
||||
# Count by file type
|
||||
extensions = {}
|
||||
for file_path in self.input_files:
|
||||
ext = Path(file_path).suffix
|
||||
extensions[ext] = extensions.get(ext, 0) + 1
|
||||
|
||||
if extensions:
|
||||
info += "\n\nFile Types:"
|
||||
for ext, count in sorted(extensions.items()):
|
||||
info += f"\n{ext or 'No extension'}: {count}"
|
||||
|
||||
self.input_info.setPlainText(info)
|
||||
|
||||
def get_input_files(self):
|
||||
"""Get list of input files."""
|
||||
return self.input_files.copy()
|
||||
|
||||
def get_output_directory(self):
|
||||
"""Get output directory path."""
|
||||
return self.output_directory
|
||||
|
|
|
|||
55
gui/main.py
55
gui/main.py
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
DazedMTLTool GUI - Main Application
|
||||
A PyQt-based graphical user interface for the DazedMTLTool translation system.
|
||||
DazedTL GUI - Main Application
|
||||
A PyQt-based graphical user interface for the DazedTL translation system.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
|
@ -29,7 +29,14 @@ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QSettings, QCoreApplic
|
|||
from PyQt5.QtGui import QIcon, QFont, QPixmap, QScreen, QGuiApplication
|
||||
|
||||
|
||||
from util.paths import PROJECT_ROOT, ICON_PATH, LAST_UPDATE_SHA_PATH
|
||||
from util.paths import (
|
||||
APP_NAME,
|
||||
ICON_PATH,
|
||||
LAST_UPDATE_SHA_PATH,
|
||||
ORG_NAME,
|
||||
PROJECT_ROOT,
|
||||
migrate_app_settings,
|
||||
)
|
||||
|
||||
|
||||
def load_application_icon() -> QIcon:
|
||||
|
|
@ -56,7 +63,7 @@ def check_tool_update() -> str | None:
|
|||
|
||||
|
||||
class BackgroundUpdateCheckThread(QThread):
|
||||
"""Checks for DazedMTLTool updates without blocking the UI."""
|
||||
"""Checks for DazedTL updates without blocking the UI."""
|
||||
|
||||
finished = pyqtSignal(object) # str | None — pending tool SHA when available
|
||||
|
||||
|
|
@ -151,7 +158,7 @@ class UpdateThread(QThread):
|
|||
@classmethod
|
||||
def fetch_latest_sha(cls) -> str:
|
||||
req = urllib.request.Request(
|
||||
cls.branch_api_url(), headers={"User-Agent": "DazedMTLTool"}
|
||||
cls.branch_api_url(), headers={"User-Agent": APP_NAME}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read())["commit"]["id"]
|
||||
|
|
@ -167,7 +174,7 @@ class UpdateThread(QThread):
|
|||
|
||||
def _download_archive(self, zip_path: Path):
|
||||
req = urllib.request.Request(
|
||||
self.archive_zip_url(), headers={"User-Agent": "DazedMTLTool"}
|
||||
self.archive_zip_url(), headers={"User-Agent": APP_NAME}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as resp, open(zip_path, "wb") as fh:
|
||||
total = int(resp.headers.get("Content-Length", 0) or 0)
|
||||
|
|
@ -367,7 +374,7 @@ class UpdateDialog(QDialog):
|
|||
|
||||
title_col = QVBoxLayout()
|
||||
title_col.setSpacing(2)
|
||||
self.title_label = QLabel("DazedMTLTool Update")
|
||||
self.title_label = QLabel(f"{APP_NAME} Update")
|
||||
self.title_label.setObjectName("updateTitle")
|
||||
self.subtitle_label = QLabel("Stay current with the latest fixes and features.")
|
||||
self.subtitle_label.setObjectName("updateDetail")
|
||||
|
|
@ -577,7 +584,7 @@ class UpdateDialog(QDialog):
|
|||
self.current_version_label.setText(sha)
|
||||
self.headline_label.setText("Update installed successfully")
|
||||
self.detail_label.setText(
|
||||
"Restart DazedMTLTool to load the new version."
|
||||
f"Restart {APP_NAME} to load the new version."
|
||||
)
|
||||
self.subtitle_label.setText("Update complete.")
|
||||
if isinstance(self.parent(), DazedMTLGUI):
|
||||
|
|
@ -620,7 +627,7 @@ from gui.skills_tab import SkillsTab
|
|||
from gui.batch_tab import BatchTab
|
||||
|
||||
class DazedMTLGUI(QMainWindow):
|
||||
"""Main GUI window for the DazedMTLTool."""
|
||||
"""Main GUI window for DazedTL."""
|
||||
|
||||
PAGE_GUIDE = 0
|
||||
PAGE_WORKFLOW = 1
|
||||
|
|
@ -631,7 +638,7 @@ class DazedMTLGUI(QMainWindow):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = QSettings("DazedTranslations", "DazedMTLTool")
|
||||
self.settings = QSettings(ORG_NAME, APP_NAME)
|
||||
self._pending_tool_sha: str | None = None
|
||||
self._update_check_thread = None
|
||||
self._shutdown_started = False
|
||||
|
|
@ -791,16 +798,16 @@ class DazedMTLGUI(QMainWindow):
|
|||
|
||||
# Update window title only when non-default scale is active
|
||||
if scale_factor != 1.0:
|
||||
self.setWindowTitle(f"DazedMTLTool - Visual Translation Interface (Font: {scale_factor:.1f}x)")
|
||||
self.setWindowTitle(f"{APP_NAME} - Visual Translation Interface (Font: {scale_factor:.1f}x)")
|
||||
else:
|
||||
self.setWindowTitle("DazedMTLTool - Visual Translation Interface")
|
||||
self.setWindowTitle(f"{APP_NAME} - Visual Translation Interface")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not apply font scaling: {e}")
|
||||
|
||||
def init_ui(self):
|
||||
"""Initialize the user interface."""
|
||||
self.setWindowTitle("DazedMTLTool - Visual Translation Interface")
|
||||
self.setWindowTitle(f"{APP_NAME} - Visual Translation Interface")
|
||||
|
||||
# Get screen geometry and set window size more responsively
|
||||
screen = QApplication.primaryScreen()
|
||||
|
|
@ -1012,7 +1019,7 @@ class DazedMTLGUI(QMainWindow):
|
|||
return container
|
||||
|
||||
def start_background_update_check(self):
|
||||
"""Check for DazedMTLTool updates after the GUI is visible."""
|
||||
"""Check for DazedTL updates after the GUI is visible."""
|
||||
if self._update_check_thread and self._update_check_thread.isRunning():
|
||||
return
|
||||
self._update_check_thread = BackgroundUpdateCheckThread(self)
|
||||
|
|
@ -1221,15 +1228,14 @@ class DazedMTLGUI(QMainWindow):
|
|||
"""Show the about dialog."""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"About DazedMTLTool GUI",
|
||||
"""
|
||||
<h3>DazedMTLTool GUI</h3>
|
||||
<p>A visual interface for the DazedMTLTool translation system.</p>
|
||||
<p>This tool helps translate visual novels, RPG games, and other text-based content using AI translation services.</p>
|
||||
f"About {APP_NAME}",
|
||||
f"""
|
||||
<h3>{APP_NAME}</h3>
|
||||
<p>An AI translation tool for visual novels, RPG games, and other text-based content.</p>
|
||||
<p><b>Features:</b></p>
|
||||
<ul>
|
||||
<li>Guided Workflow for RPG Maker and WolfDawn</li>
|
||||
<li>Visual configuration management</li>
|
||||
<li>Module-specific settings</li>
|
||||
<li>Real-time translation monitoring</li>
|
||||
<li>File management and organization</li>
|
||||
</ul>
|
||||
|
|
@ -1269,10 +1275,11 @@ def main():
|
|||
|
||||
install_qt_message_filter()
|
||||
ensure_linux_desktop_entry()
|
||||
QGuiApplication.setDesktopFileName("DazedMTLTool")
|
||||
QGuiApplication.setDesktopFileName(APP_NAME)
|
||||
|
||||
QCoreApplication.setOrganizationName("DazedTranslations")
|
||||
QCoreApplication.setApplicationName("DazedMTLTool")
|
||||
QCoreApplication.setOrganizationName(ORG_NAME)
|
||||
QCoreApplication.setApplicationName(APP_NAME)
|
||||
migrate_app_settings()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
|
|
@ -1283,7 +1290,7 @@ def main():
|
|||
try:
|
||||
import ctypes
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
||||
"DazedTranslations.DazedMTLTool.1"
|
||||
f"{ORG_NAME}.{APP_NAME}.1"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -101,7 +101,7 @@ from util.wolfdawn import wrap_search as wolf_ws
|
|||
import util.dazedwrap as dazedwrap
|
||||
|
||||
from gui.setup_skills_editors import SetupSkillsEditors
|
||||
from util.paths import PROJECT_ROOT, VOCAB_PATH
|
||||
from util.paths import PROJECT_ROOT, VOCAB_PATH, APP_NAME, ORG_NAME
|
||||
from util.project_scanner import (
|
||||
detect_wolf_layout,
|
||||
find_wolf_text_archives,
|
||||
|
|
@ -208,7 +208,7 @@ class WolfWorkflowTab(QWidget):
|
|||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
try:
|
||||
self.settings = QSettings("DazedTranslations", "DazedMTLTool")
|
||||
self.settings = QSettings(ORG_NAME, APP_NAME)
|
||||
except Exception:
|
||||
self.settings = None
|
||||
|
||||
|
|
@ -3670,7 +3670,7 @@ class WolfWorkflowTab(QWidget):
|
|||
self._run_task(task, on_done=_after)
|
||||
|
||||
def _tool_root(self) -> Path:
|
||||
"""DazedMTLTool project root (``files/``, ``translated/``, etc.)."""
|
||||
"""DazedTL project root (``files/``, ``translated/``, etc.)."""
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
def _translated_path(self, json_name: str) -> Path | None:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import sys
|
|||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from util.paths import VOCAB_PATH
|
||||
from util.paths import VOCAB_PATH, APP_NAME, ORG_NAME
|
||||
from util.skills import load_project_setup
|
||||
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
|
||||
|
||||
|
|
@ -638,7 +638,7 @@ class WorkflowTab(QWidget):
|
|||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
try:
|
||||
self.settings = QSettings("DazedTranslations", "DazedMTLTool")
|
||||
self.settings = QSettings(ORG_NAME, APP_NAME)
|
||||
except Exception:
|
||||
self.settings = None
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# Launcher for DazedMTLTool.desktop (finds venv, then starts the GUI).
|
||||
# Launcher for DazedTL.desktop (finds venv, then starts the GUI).
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT" || exit 1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Launch script for DazedMTLTool GUI."""
|
||||
"""Launch script for DazedTL GUI."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -40,7 +40,7 @@ def check_dependencies():
|
|||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
print("DazedMTLTool GUI Launcher")
|
||||
print("DazedTL GUI Launcher")
|
||||
print("=" * 40)
|
||||
|
||||
from util.paths import ensure_vocab_file, migrate_root_data_files
|
||||
|
|
|
|||
2
tests/fixtures/README.md
vendored
2
tests/fixtures/README.md
vendored
|
|
@ -35,7 +35,7 @@ Or explicitly:
|
|||
Manual equivalent (must run from project root, with venv activated):
|
||||
|
||||
```bash
|
||||
cd /path/to/DazedMTLTool
|
||||
cd /path/to/DazedTL
|
||||
source .venv/bin/activate # or: source venv/bin/activate
|
||||
python -m unittest tests.test_mvmz_source_original -v
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# Run DazedMTLTool unit tests using the project venv (cwd = project root).
|
||||
# Run DazedTL unit tests using the project venv (cwd = project root).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ class WolfBundledOnlyTests(unittest.TestCase):
|
|||
with patch("util.wolfdawn.bundled_binary_path", return_value=missing):
|
||||
with self.assertRaises(WolfDawnError) as ctx:
|
||||
ensure_wolf_binary()
|
||||
self.assertIn("Update DazedMTLTool", str(ctx.exception))
|
||||
self.assertIn("Update DazedTL", str(ctx.exception))
|
||||
|
||||
def test_ensure_wolf_binary_never_downloads(self):
|
||||
missing = Path("/nonexistent/wolf/missing")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RV2JSON.exe — https://github.com/Sinflower/RV2JSON (bin/RV2JSON.exe)
|
|||
Decrypter CLI — https://github.com/uuksu/RPGMakerDecrypter (release asset)
|
||||
|
||||
End users receive curated copies via the offline bundle (``util/ace/offline/``)
|
||||
shipped with DazedMTLTool updates. Upstream fetches are maintainer-only
|
||||
shipped with DazedTL updates. Upstream fetches are maintainer-only
|
||||
(``--refresh-offline`` or ``--force``).
|
||||
"""
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ DECRYPTER_LOCAL = ACE_DIR / "RPGMakerDecrypter-cli.exe"
|
|||
# Legacy name shipped in older DazedMTLTool commits (local only, not in git).
|
||||
DECRYPTER_LEGACY = ACE_DIR / "RPGMakerDecrypter.exe"
|
||||
|
||||
USER_AGENT = "DazedMTLTool"
|
||||
USER_AGENT = "DazedTL"
|
||||
|
||||
|
||||
def _load_versions() -> dict:
|
||||
|
|
@ -184,7 +184,7 @@ def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
|
|||
if RV2JSON_LOCAL.is_file():
|
||||
return True
|
||||
_log(
|
||||
f"ERROR: RV2JSON.exe not found. Update DazedMTLTool or ask the maintainer "
|
||||
f"ERROR: RV2JSON.exe not found. Update DazedTL or ask the maintainer "
|
||||
f"to refresh util/ace/offline/RV2JSON.exe.",
|
||||
log_fn,
|
||||
)
|
||||
|
|
@ -207,7 +207,7 @@ def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
|
|||
if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
|
||||
return True
|
||||
_log(
|
||||
f"ERROR: {DECRYPTER_ASSET} not found. Update DazedMTLTool or ask the maintainer "
|
||||
f"ERROR: {DECRYPTER_ASSET} not found. Update DazedTL or ask the maintainer "
|
||||
f"to refresh util/ace/offline/{DECRYPTER_ASSET}.",
|
||||
log_fn,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,217 +1,217 @@
|
|||
"""Download / update Forge plugins from len's upstream repo (gitgud.io).
|
||||
|
||||
Upstream: https://gitgud.io/zero64801/forge-mvmz
|
||||
CI builds a unified forge.js plugin (master branch artifacts) for MZ.
|
||||
|
||||
RPG Maker MV ships with NW.js / Chrome ~65, which cannot run the rewritten
|
||||
Svelte Forge bundle. MV therefore keeps the pre-rewrite legacy plugin
|
||||
(``Forge_MV.js`` / ``upstream/Forge_MV.js`` / ``legacy/Forge_MV.js``).
|
||||
|
||||
Offline copies: util/forge/upstream/
|
||||
Active plugins: util/forge/Forge_MZ.js (modern), util/forge/Forge_MV.js (legacy)
|
||||
|
||||
End users receive curated copies shipped with DazedMTLTool updates.
|
||||
Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``)
|
||||
and only refresh the MZ modern plugin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
_PKG_ROOT = Path(__file__).resolve().parent
|
||||
UPSTREAM_DIR = _PKG_ROOT / "upstream"
|
||||
|
||||
PLUGIN_BY_ENGINE = {
|
||||
"MV": "Forge_MV",
|
||||
"MZ": "Forge_MZ",
|
||||
}
|
||||
|
||||
FORGE_PROJECT = "zero64801/forge-mvmz"
|
||||
FORGE_BRANCH = "master"
|
||||
GITGUD_API = "https://gitgud.io/api/v4"
|
||||
VERSION_FILE = _PKG_ROOT / ".forge_version.json"
|
||||
USER_AGENT = "DazedMTLTool"
|
||||
|
||||
|
||||
def bundled_plugin_path(engine: str) -> Path:
|
||||
name = PLUGIN_BY_ENGINE.get(engine)
|
||||
if not name:
|
||||
raise ValueError(f"Unsupported engine: {engine}")
|
||||
return _PKG_ROOT / f"{name}.js"
|
||||
|
||||
|
||||
def upstream_plugin_path(engine: str) -> Path:
|
||||
return UPSTREAM_DIR / f"{PLUGIN_BY_ENGINE[engine]}.js"
|
||||
|
||||
|
||||
def _load_versions() -> dict:
|
||||
if not VERSION_FILE.is_file():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(VERSION_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _save_versions(data: dict) -> None:
|
||||
VERSION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _api_json(url: str) -> dict | list:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def _download(url: str, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = dest.with_suffix(dest.suffix + ".part")
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=600) as resp, open(tmp, "wb") as fh:
|
||||
shutil.copyfileobj(resp, fh)
|
||||
tmp.replace(dest)
|
||||
|
||||
|
||||
def _project_id() -> str:
|
||||
return urllib.parse.quote(FORGE_PROJECT, safe="")
|
||||
|
||||
|
||||
def _upstream_commit() -> str:
|
||||
url = f"{GITGUD_API}/projects/{_project_id()}/repository/commits/{FORGE_BRANCH}"
|
||||
data = _api_json(url)
|
||||
commit = data.get("id") or data.get("sha") or ""
|
||||
if not commit:
|
||||
raise RuntimeError(f"Could not resolve upstream commit for {FORGE_PROJECT}")
|
||||
return commit
|
||||
|
||||
|
||||
def _artifact_url(filename: str) -> str:
|
||||
"""Latest successful build_job artifact on the tracked branch."""
|
||||
return (
|
||||
f"https://gitgud.io/{FORGE_PROJECT}/-/jobs/artifacts/{FORGE_BRANCH}"
|
||||
f"/raw/{filename}?job=build_job"
|
||||
)
|
||||
|
||||
|
||||
def _log(msg: str, log_fn) -> None:
|
||||
if log_fn:
|
||||
log_fn(msg)
|
||||
else:
|
||||
print(msg, flush=True)
|
||||
|
||||
|
||||
def _seed_from_offline(engine: str, log_fn) -> bool:
|
||||
"""Copy bundled offline copy into the active plugin path if missing."""
|
||||
dest = bundled_plugin_path(engine)
|
||||
if dest.is_file():
|
||||
return True
|
||||
offline = upstream_plugin_path(engine)
|
||||
if not offline.is_file():
|
||||
return False
|
||||
shutil.copy2(offline, dest)
|
||||
if log_fn:
|
||||
_log(f"Using offline bundled {dest.name}", log_fn)
|
||||
return True
|
||||
|
||||
|
||||
def _fetch_plugins(log_fn) -> bytes:
|
||||
"""Download the unified forge.js CI artifact."""
|
||||
_log(f"Downloading forge.js from {FORGE_PROJECT} ({FORGE_BRANCH} CI)...", log_fn)
|
||||
return _download_bytes(_artifact_url("forge.js"))
|
||||
|
||||
|
||||
def _install_modern_mz_bytes(data: bytes) -> None:
|
||||
"""Write modern forge.js into MZ active + offline paths only (never MV)."""
|
||||
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
|
||||
bundled_plugin_path("MZ").write_bytes(data)
|
||||
upstream_plugin_path("MZ").write_bytes(data)
|
||||
|
||||
|
||||
def _download_bytes(url: str) -> bytes:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=600) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def refresh_forge_plugins(log_fn=print) -> bool:
|
||||
"""Download len's latest forge.js and refresh the MZ modern plugin only."""
|
||||
try:
|
||||
commit = _upstream_commit()
|
||||
except Exception as exc:
|
||||
_log(f"ERROR: could not contact upstream ({exc})", log_fn)
|
||||
return False
|
||||
|
||||
try:
|
||||
data = _fetch_plugins(log_fn)
|
||||
except Exception as exc:
|
||||
_log(f"ERROR: download failed for forge.js ({exc})", log_fn)
|
||||
return False
|
||||
|
||||
if not data.strip().startswith(b"//"):
|
||||
_log("ERROR: forge.js download did not look like a plugin file.", log_fn)
|
||||
return False
|
||||
|
||||
_install_modern_mz_bytes(data)
|
||||
|
||||
versions = _load_versions()
|
||||
versions["commit"] = commit
|
||||
versions["branch"] = FORGE_BRANCH
|
||||
_save_versions(versions)
|
||||
_log(
|
||||
f"Forge MZ plugin updated ({commit[:12]}). "
|
||||
"MV keeps the legacy Chrome-65-compatible plugin.",
|
||||
log_fn,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def seed_forge_plugins(log_fn=None) -> None:
|
||||
"""Copy offline-bundled Forge plugins if missing (no network)."""
|
||||
for engine in PLUGIN_BY_ENGINE:
|
||||
_seed_from_offline(engine, log_fn)
|
||||
|
||||
|
||||
def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure Forge plugins are present from the offline bundle (no upstream fetch)."""
|
||||
for engine in PLUGIN_BY_ENGINE:
|
||||
_seed_from_offline(engine, log_fn)
|
||||
|
||||
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
|
||||
if missing and not force:
|
||||
names = ", ".join(PLUGIN_BY_ENGINE[e] + ".js" for e in missing)
|
||||
_log(
|
||||
f"ERROR: Forge plugin(s) missing ({names}). "
|
||||
"Update DazedMTLTool or ask the maintainer to refresh util/forge/upstream/.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
||||
if force:
|
||||
if refresh_forge_plugins(log_fn=log_fn):
|
||||
return True
|
||||
have_local = all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
|
||||
if not have_local:
|
||||
_log("ERROR: Forge plugin update failed.", log_fn)
|
||||
return False
|
||||
_log("Warning: could not update Forge plugins; using local copy.", log_fn)
|
||||
|
||||
return all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if "--refresh-offline" in sys.argv:
|
||||
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ok = refresh_forge_plugins()
|
||||
return 0 if ok else 1
|
||||
force = "--force" in sys.argv or "-f" in sys.argv
|
||||
return 0 if ensure_forge_plugins(force=force) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
"""Download / update Forge plugins from len's upstream repo (gitgud.io).
|
||||
|
||||
Upstream: https://gitgud.io/zero64801/forge-mvmz
|
||||
CI builds a unified forge.js plugin (master branch artifacts) for MZ.
|
||||
|
||||
RPG Maker MV ships with NW.js / Chrome ~65, which cannot run the rewritten
|
||||
Svelte Forge bundle. MV therefore keeps the pre-rewrite legacy plugin
|
||||
(``Forge_MV.js`` / ``upstream/Forge_MV.js`` / ``legacy/Forge_MV.js``).
|
||||
|
||||
Offline copies: util/forge/upstream/
|
||||
Active plugins: util/forge/Forge_MZ.js (modern), util/forge/Forge_MV.js (legacy)
|
||||
|
||||
End users receive curated copies shipped with DazedTL updates.
|
||||
Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``)
|
||||
and only refresh the MZ modern plugin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
_PKG_ROOT = Path(__file__).resolve().parent
|
||||
UPSTREAM_DIR = _PKG_ROOT / "upstream"
|
||||
|
||||
PLUGIN_BY_ENGINE = {
|
||||
"MV": "Forge_MV",
|
||||
"MZ": "Forge_MZ",
|
||||
}
|
||||
|
||||
FORGE_PROJECT = "zero64801/forge-mvmz"
|
||||
FORGE_BRANCH = "master"
|
||||
GITGUD_API = "https://gitgud.io/api/v4"
|
||||
VERSION_FILE = _PKG_ROOT / ".forge_version.json"
|
||||
USER_AGENT = "DazedTL"
|
||||
|
||||
|
||||
def bundled_plugin_path(engine: str) -> Path:
|
||||
name = PLUGIN_BY_ENGINE.get(engine)
|
||||
if not name:
|
||||
raise ValueError(f"Unsupported engine: {engine}")
|
||||
return _PKG_ROOT / f"{name}.js"
|
||||
|
||||
|
||||
def upstream_plugin_path(engine: str) -> Path:
|
||||
return UPSTREAM_DIR / f"{PLUGIN_BY_ENGINE[engine]}.js"
|
||||
|
||||
|
||||
def _load_versions() -> dict:
|
||||
if not VERSION_FILE.is_file():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(VERSION_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _save_versions(data: dict) -> None:
|
||||
VERSION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _api_json(url: str) -> dict | list:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def _download(url: str, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = dest.with_suffix(dest.suffix + ".part")
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=600) as resp, open(tmp, "wb") as fh:
|
||||
shutil.copyfileobj(resp, fh)
|
||||
tmp.replace(dest)
|
||||
|
||||
|
||||
def _project_id() -> str:
|
||||
return urllib.parse.quote(FORGE_PROJECT, safe="")
|
||||
|
||||
|
||||
def _upstream_commit() -> str:
|
||||
url = f"{GITGUD_API}/projects/{_project_id()}/repository/commits/{FORGE_BRANCH}"
|
||||
data = _api_json(url)
|
||||
commit = data.get("id") or data.get("sha") or ""
|
||||
if not commit:
|
||||
raise RuntimeError(f"Could not resolve upstream commit for {FORGE_PROJECT}")
|
||||
return commit
|
||||
|
||||
|
||||
def _artifact_url(filename: str) -> str:
|
||||
"""Latest successful build_job artifact on the tracked branch."""
|
||||
return (
|
||||
f"https://gitgud.io/{FORGE_PROJECT}/-/jobs/artifacts/{FORGE_BRANCH}"
|
||||
f"/raw/{filename}?job=build_job"
|
||||
)
|
||||
|
||||
|
||||
def _log(msg: str, log_fn) -> None:
|
||||
if log_fn:
|
||||
log_fn(msg)
|
||||
else:
|
||||
print(msg, flush=True)
|
||||
|
||||
|
||||
def _seed_from_offline(engine: str, log_fn) -> bool:
|
||||
"""Copy bundled offline copy into the active plugin path if missing."""
|
||||
dest = bundled_plugin_path(engine)
|
||||
if dest.is_file():
|
||||
return True
|
||||
offline = upstream_plugin_path(engine)
|
||||
if not offline.is_file():
|
||||
return False
|
||||
shutil.copy2(offline, dest)
|
||||
if log_fn:
|
||||
_log(f"Using offline bundled {dest.name}", log_fn)
|
||||
return True
|
||||
|
||||
|
||||
def _fetch_plugins(log_fn) -> bytes:
|
||||
"""Download the unified forge.js CI artifact."""
|
||||
_log(f"Downloading forge.js from {FORGE_PROJECT} ({FORGE_BRANCH} CI)...", log_fn)
|
||||
return _download_bytes(_artifact_url("forge.js"))
|
||||
|
||||
|
||||
def _install_modern_mz_bytes(data: bytes) -> None:
|
||||
"""Write modern forge.js into MZ active + offline paths only (never MV)."""
|
||||
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
|
||||
bundled_plugin_path("MZ").write_bytes(data)
|
||||
upstream_plugin_path("MZ").write_bytes(data)
|
||||
|
||||
|
||||
def _download_bytes(url: str) -> bytes:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=600) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def refresh_forge_plugins(log_fn=print) -> bool:
|
||||
"""Download len's latest forge.js and refresh the MZ modern plugin only."""
|
||||
try:
|
||||
commit = _upstream_commit()
|
||||
except Exception as exc:
|
||||
_log(f"ERROR: could not contact upstream ({exc})", log_fn)
|
||||
return False
|
||||
|
||||
try:
|
||||
data = _fetch_plugins(log_fn)
|
||||
except Exception as exc:
|
||||
_log(f"ERROR: download failed for forge.js ({exc})", log_fn)
|
||||
return False
|
||||
|
||||
if not data.strip().startswith(b"//"):
|
||||
_log("ERROR: forge.js download did not look like a plugin file.", log_fn)
|
||||
return False
|
||||
|
||||
_install_modern_mz_bytes(data)
|
||||
|
||||
versions = _load_versions()
|
||||
versions["commit"] = commit
|
||||
versions["branch"] = FORGE_BRANCH
|
||||
_save_versions(versions)
|
||||
_log(
|
||||
f"Forge MZ plugin updated ({commit[:12]}). "
|
||||
"MV keeps the legacy Chrome-65-compatible plugin.",
|
||||
log_fn,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def seed_forge_plugins(log_fn=None) -> None:
|
||||
"""Copy offline-bundled Forge plugins if missing (no network)."""
|
||||
for engine in PLUGIN_BY_ENGINE:
|
||||
_seed_from_offline(engine, log_fn)
|
||||
|
||||
|
||||
def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure Forge plugins are present from the offline bundle (no upstream fetch)."""
|
||||
for engine in PLUGIN_BY_ENGINE:
|
||||
_seed_from_offline(engine, log_fn)
|
||||
|
||||
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
|
||||
if missing and not force:
|
||||
names = ", ".join(PLUGIN_BY_ENGINE[e] + ".js" for e in missing)
|
||||
_log(
|
||||
f"ERROR: Forge plugin(s) missing ({names}). "
|
||||
"Update DazedTL or ask the maintainer to refresh util/forge/upstream/.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
||||
if force:
|
||||
if refresh_forge_plugins(log_fn=log_fn):
|
||||
return True
|
||||
have_local = all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
|
||||
if not have_local:
|
||||
_log("ERROR: Forge plugin update failed.", log_fn)
|
||||
return False
|
||||
_log("Warning: could not update Forge plugins; using local copy.", log_fn)
|
||||
|
||||
return all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if "--refresh-offline" in sys.argv:
|
||||
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ok = refresh_forge_plugins()
|
||||
return 0 if ok else 1
|
||||
force = "--force" in sys.argv or "-f" in sys.argv
|
||||
return 0 if ensure_forge_plugins(force=force) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def format_patch_config(
|
|||
branch = (branch or "").strip() or DEFAULT_BRANCH
|
||||
|
||||
lines = [
|
||||
"# Generated by DazedMTLTool from Config → Game Update defaults.",
|
||||
"# Generated by DazedTL from Config → Game Update defaults.",
|
||||
"# Edit repo= per game. forge/host/username usually stay the same.",
|
||||
f"forge={forge}",
|
||||
f"host={host}",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import subprocess
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from util.paths import ICON_PATH, PROJECT_ROOT
|
||||
from util.paths import APP_NAME, ICON_PATH, LEGACY_APP_NAME, PROJECT_ROOT
|
||||
|
||||
DESKTOP_ID = "DazedMTLTool"
|
||||
DESKTOP_ID = APP_NAME
|
||||
LAUNCH_SCRIPT = PROJECT_ROOT / "scripts" / "launch.sh"
|
||||
|
||||
# Qt logs this on Wayland whenever a dialog tries to steal focus (harmless noise).
|
||||
|
|
@ -63,7 +63,7 @@ def _desktop_content(root: Path, icon: Path, launch: Path) -> str:
|
|||
"[Desktop Entry]\n"
|
||||
"Type=Application\n"
|
||||
f"Name={DESKTOP_ID}\n"
|
||||
"GenericName=Translation Tool\n"
|
||||
"GenericName=AI Translation Tool\n"
|
||||
"Comment=AI translation tool for visual novels and RPG games\n"
|
||||
f"Exec={launch}\n"
|
||||
f"Icon={icon}\n"
|
||||
|
|
@ -75,7 +75,7 @@ def _desktop_content(root: Path, icon: Path, launch: Path) -> str:
|
|||
|
||||
|
||||
def ensure_linux_desktop_entry() -> None:
|
||||
"""Write ~/.local/share/applications/DazedMTLTool.desktop with absolute paths."""
|
||||
"""Write ~/.local/share/applications/DazedTL.desktop with absolute paths."""
|
||||
if not sys.platform.startswith("linux"):
|
||||
return
|
||||
if not LAUNCH_SCRIPT.is_file() or not ICON_PATH.is_file():
|
||||
|
|
@ -85,6 +85,13 @@ def ensure_linux_desktop_entry() -> None:
|
|||
content = _desktop_content(PROJECT_ROOT, ICON_PATH, LAUNCH_SCRIPT)
|
||||
|
||||
try:
|
||||
legacy = desktop_path.parent / f"{LEGACY_APP_NAME}.desktop"
|
||||
if legacy.is_file() and legacy.resolve() != desktop_path.resolve():
|
||||
try:
|
||||
legacy.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if desktop_path.is_file() and desktop_path.read_text(encoding="utf-8") == content:
|
||||
return
|
||||
desktop_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
# Product identity (QSettings / desktop / window titles).
|
||||
ORG_NAME = "DazedTranslations"
|
||||
APP_NAME = "DazedTL"
|
||||
LEGACY_APP_NAME = "DazedMTLTool"
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = PROJECT_ROOT / "data"
|
||||
VOCAB_PATH = DATA_DIR / "vocab.txt"
|
||||
|
|
@ -55,6 +60,26 @@ def migrate_prompt_to_skills() -> None:
|
|||
return
|
||||
|
||||
|
||||
def migrate_app_settings() -> None:
|
||||
"""Copy QSettings from the legacy app name into DazedTL once."""
|
||||
try:
|
||||
from PyQt5.QtCore import QSettings
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
new = QSettings(ORG_NAME, APP_NAME)
|
||||
if str(new.value("_migrated_from_legacy_app", "")) == "1":
|
||||
return
|
||||
|
||||
old = QSettings(ORG_NAME, LEGACY_APP_NAME)
|
||||
old_keys = list(old.allKeys())
|
||||
new_keys = [k for k in new.allKeys() if k != "_migrated_from_legacy_app"]
|
||||
if old_keys and not new_keys:
|
||||
for key in old_keys:
|
||||
new.setValue(key, old.value(key))
|
||||
new.setValue("_migrated_from_legacy_app", "1")
|
||||
|
||||
|
||||
migrate_root_data_files()
|
||||
migrate_prompt_to_skills()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Shared translation utilities for DazedMTLTool.
|
||||
Shared translation utilities for DazedTL.
|
||||
Centralized translation function used across all modules.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"""WolfDawn CLI bootstrap and thin subprocess wrappers.
|
||||
|
||||
WolfDawn is the Rust toolchain (https://gitgud.io/zero64801/wolfdawn) that
|
||||
unpacks, extracts, injects, and repacks WOLF RPG Editor game data. DazedMTLTool
|
||||
unpacks, extracts, injects, and repacks WOLF RPG Editor game data. DazedTL
|
||||
ships prebuilt ``wolf`` binaries offline under ``util/wolfdawn/bin/<platform>/``
|
||||
so end users never need a Rust toolchain or a live upstream fetch. Binaries are
|
||||
updated when DazedMTLTool itself is updated. Everything the Wolf workflow needs
|
||||
updated when DazedTL itself is updated. Everything the Wolf workflow needs
|
||||
goes through the helpers here so command syntax and exit-code handling live in
|
||||
one place.
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ _RELEASE_ASSET_MATCH = {
|
|||
}
|
||||
_DOWNLOAD_UA = (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"DazedMTLTool WolfDawn-fetch"
|
||||
"DazedTL WolfDawn-fetch"
|
||||
)
|
||||
|
||||
PathLike = Union[str, Path]
|
||||
|
|
@ -394,7 +394,7 @@ def ensure_wolf_binary(log_fn=print) -> Path:
|
|||
return bundled
|
||||
raise WolfDawnError(
|
||||
f"No bundled WolfDawn binary for '{_platform_dir()}' at {bundled}. "
|
||||
"Update DazedMTLTool to receive a prebuilt wolf binary."
|
||||
"Update DazedTL to receive a prebuilt wolf binary."
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Check for and apply WolfDawn ``wolf`` binary updates (maintainer-only upstream fetch).
|
||||
|
||||
End users receive prebuilt binaries under ``util/wolfdawn/bin/<platform>/`` via
|
||||
DazedMTLTool updates. Maintainers refresh them with ``--refresh-all`` or ``--force``.
|
||||
DazedTL updates. Maintainers refresh them with ``--refresh-all`` or ``--force``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -150,7 +150,7 @@ def ensure_wolfdawn_binary(force: bool = False, log_fn=print) -> bool:
|
|||
return True
|
||||
_log(
|
||||
f"ERROR: no bundled WolfDawn binary for '{platform}' at {bundled}. "
|
||||
"Update DazedMTLTool to receive a prebuilt wolf binary.",
|
||||
"Update DazedTL to receive a prebuilt wolf binary.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
|
|
|||
Loading…
Reference in a new issue