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:
DazedAnon 2026-07-22 12:27:22 -05:00
parent 41691879dc
commit 4d1b01c870
30 changed files with 4353 additions and 4311 deletions

View file

@ -1,10 +1,10 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=DazedMTLTool Name=DazedTL
GenericName=Translation Tool GenericName=AI Translation Tool
Comment=AI translation tool for visual novels and RPG games Comment=AI translation tool for visual novels and RPG games
Exec=bash -c 'exec "$(cd "$(dirname "%k")" && pwd)/scripts/launch.sh"' Exec=bash -c 'exec "$(cd "$(dirname "%k")" && pwd)/scripts/launch.sh"'
Icon=%k/assets/icon.png Icon=%k/assets/icon.png
Terminal=false Terminal=false
Categories=Development;Utility; Categories=Development;Utility;
StartupWMClass=DazedMTLTool StartupWMClass=DazedTL

View file

@ -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. 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 ## 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. - **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). - **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. Download the Tool
1. Click the green **Code** button at the top of this page and select **Download ZIP**. 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 ### 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. **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. 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) | | **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 | | **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. > **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 ### 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 //"* - *"Add a new regex pattern to skip lines that start with //"*
- *"Change the wordwrap logic to break on full-width punctuation"* - *"Change the wordwrap logic to break on full-width punctuation"*

View file

@ -2,7 +2,7 @@
setlocal EnableDelayedExpansion setlocal EnableDelayedExpansion
echo ========================================== echo ==========================================
echo DazedMTLTool Startup Script echo DazedTL Startup Script
echo ========================================== echo ==========================================
echo. echo.
@ -180,7 +180,7 @@ echo.
:: Launch the GUI :: Launch the GUI
echo ========================================== echo ==========================================
echo Launching DazedMTLTool GUI... echo Launching DazedTL GUI...
echo ========================================== echo ==========================================
echo. echo.

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# DazedMTLTool startup script for Linux/macOS # DazedTL startup script for Linux/macOS
set -o pipefail set -o pipefail
@ -150,7 +150,7 @@ ensure_vocab_file() {
} }
echo "==========================================" echo "=========================================="
echo " DazedMTLTool Startup Script" echo " DazedTL Startup Script"
echo "==========================================" echo "=========================================="
echo echo
@ -222,7 +222,7 @@ fi
echo echo
echo "==========================================" echo "=========================================="
echo " Launching DazedMTLTool GUI..." echo " Launching DazedTL GUI..."
echo "==========================================" echo "=========================================="
echo echo

View file

@ -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), 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. plus modules for Ren'Py, Tyrano, Kirikiri, CSV, and more.

View file

@ -69,7 +69,7 @@ Use either:
### Example: open the game in Cursor ### Example: open the game in Cursor
1. In Cursor: **File → Open Folder** and select the game root (where `Game.exe` or Wolf `Data` lives). 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). 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. 4. Paste the agent's labeled blocks back into the Workflow editors.

View file

@ -14,7 +14,7 @@ You just downloaded a small MV game. You want English dialogue, menus that make
### 1. Get the tool ready ### 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. 2. Open **Configuration**, set your API key and model, save.
3. Open the game folder in Cursor so the agent can read JSON later. 3. Open the game folder in Cursor so the agent can read JSON later.

View file

@ -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. 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) ### Game skill rules (for `game_skill` block)
Produce the per-game translation skill saved at `skills/game.md`. 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): **Translation Frame only** (one compact line per field; evidence-based):
- `世界観 (Theme / setting)` - genre, world type, core atmosphere - `世界観 (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) ### Game skill rules (for `game_skill` block)
Produce the per-game translation skill saved at `skills/game.md`. 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): **Translation Frame only** (one compact line per field; evidence-based):
- `世界観 (Theme / setting)` - genre, world type, core atmosphere - `世界観 (Theme / setting)` - genre, world type, core atmosphere

View file

@ -2,7 +2,7 @@
setlocal EnableExtensions EnableDelayedExpansion setlocal EnableExtensions EnableDelayedExpansion
REM When deployed on a game: this file sits in the game root; patch scripts live in .\gameupdate\ 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" set "GU_ROOT=%~dp0"
REM Game root is this batch file's folder (not %%CD%%, so full-path launches still work). REM Game root is this batch file's folder (not %%CD%%, so full-path launches still work).
set "GAME_ROOT=!GU_ROOT!" set "GAME_ROOT=!GU_ROOT!"

View file

@ -1,159 +1,159 @@
# Apply Patch # Apply Patch
1. Click Code 1. Click Code
2. Click Download ZIP 2. Click Download ZIP
3. Extract to game folder and Replace All. 3. Extract to game folder and Replace All.
## Future Patching ## Future Patching
1. Run **`GameUpdate.bat`** to auto patch (Windows). 1. Run **`GameUpdate.bat`** to auto patch (Windows).
### Folder layout ### 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/`**. **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. **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\`. **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. 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. 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 ### patch-config.txt
Create `gameupdate/patch-config.txt` next to `patch.ps1` (see `patch-config.example.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. 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 ```txt
forge=gitlab forge=gitlab
host=gitgud.io host=gitgud.io
username=YOUR_ORG_OR_USER username=YOUR_ORG_OR_USER
repo=YOUR_PATCH_REPO repo=YOUR_PATCH_REPO
branch=main branch=main
``` ```
| Key | Meaning | | Key | Meaning |
|-----|---------| |-----|---------|
| `forge` | `gitlab` (default), `github`, or `forgejo` (`gitea` also accepted) | | `forge` | `gitlab` (default), `github`, or `forgejo` (`gitea` also accepted) |
| `host` | Hostname only. Defaults: `gitgud.io` / `github.com` / `codeberg.org` | | `host` | Hostname only. Defaults: `gitgud.io` / `github.com` / `codeberg.org` |
| `username` | Owner / org / group (`owner=` / `org=` aliases work) | | `username` | Owner / org / group (`owner=` / `org=` aliases work) |
| `repo` | Repository name | | `repo` | Repository name |
| `branch` | Branch to track | | `branch` | Branch to track |
Examples: Examples:
```txt ```txt
# GitLab / gitgud # GitLab / gitgud
forge=gitlab forge=gitlab
host=gitgud.io host=gitgud.io
username=myorg username=myorg
repo=cool-game-en repo=cool-game-en
branch=main branch=main
# GitHub # GitHub
forge=github forge=github
host=github.com host=github.com
username=myorg username=myorg
repo=cool-game-en repo=cool-game-en
branch=main branch=main
# Forgejo / Codeberg # Forgejo / Codeberg
forge=forgejo forge=forgejo
host=codeberg.org host=codeberg.org
username=myorg username=myorg
repo=cool-game-en repo=cool-game-en
branch=main branch=main
``` ```
Older configs with only `username` / `repo` / `branch` still work (assumes GitLab on `gitgud.io`). Older configs with only `username` / `repo` / `branch` still work (assumes GitLab on `gitgud.io`).
# Troubleshooting # Troubleshooting
**GAMEUPDATE.bat doesn't update and closes immediately** **GAMEUPDATE.bat doesn't update and closes immediately**
1. Make sure your path doesn't contain any Japanese characters or lots of whitespace. 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 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. 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 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. 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. 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. 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 # Edit/Contribute
TLDR 3 steps. TLDR 3 steps.
Fork the repository. Fork the repository.
Make the changes. Make the changes.
Submit a merge request. Submit a merge request.
If everything looks good and doesn't break things I'll merge it in. If everything looks good and doesn't break things I'll merge it in.
Longer Version: Longer Version:
# Required Software: # Required Software:
* [VSCode](https://code.visualstudio.com/) Make sure you check all the boxes for context menus. * [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) * [Git](https://git-scm.com/downloads) (Use the default for everything. Just keep clicking Next)
# Guide to contributing # Guide to contributing
### 1. Fork the Repository ### 1. Fork the Repository
- Go to the repository you want to fork. - Go to the repository you want to fork.
- Click the "Fork" button. - Click the "Fork" button.
### 2. Clone Your Fork ### 2. Clone Your Fork
- Clone your forked repository to your local machine. - Clone your forked repository to your local machine.
```sh ```sh
git clone https://gitgud.io/YOUR_USERNAME/REPO_NAME.git git clone https://gitgud.io/YOUR_USERNAME/REPO_NAME.git
``` ```
### 3. Make Your Changes (In VSCode) ### 3. Make Your Changes (In VSCode)
- Edit the files locally on your new branch using VSCode. - Edit the files locally on your new branch using VSCode.
- Add and commit your changes. - Add and commit your changes.
```sh ```sh
git add . git add .
git commit -m "Description of your changes" git commit -m "Description of your changes"
``` ```
### 4. Push Your Changes ### 4. Push Your Changes
- Push your changes to your fork on GitGud.io. - Push your changes to your fork on GitGud.io.
```sh ```sh
git push origin your-feature-branch git push origin your-feature-branch
``` ```
### 5. Create a Merge Request ### 5. Create a Merge Request
- Go to your fork on GitGud.io. - Go to your fork on GitGud.io.
- Click on "Merge Requests" in the sidebar. - Click on "Merge Requests" in the sidebar.
- Click the "New merge request" button. - Click the "New merge request" button.
- Select the branch you made changes to and the target project (the original repo). - 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. - Provide a title and description for your merge request and submit it.
--- ---
## Example ## Example
Assuming you want to fork a repository named `example-project`: Assuming you want to fork a repository named `example-project`:
### 1. Fork the Repo ### 1. Fork the Repo
- Navigate to `https://gitgud.io/original_user/example-project` and click "Fork". - Navigate to `https://gitgud.io/original_user/example-project` and click "Fork".
### 2. Clone Your Fork ### 2. Clone Your Fork
```sh ```sh
git clone https://gitgud.io/YOUR_USERNAME/example-project.git git clone https://gitgud.io/YOUR_USERNAME/example-project.git
``` ```
### 3. Make Changes and Commit ### 3. Make Changes and Commit
```sh ```sh
# Make changes to the files # Make changes to the files
git add . git add .
git commit -m "Add new feature to example project" git commit -m "Add new feature to example project"
``` ```
### 4. Push Changes ### 4. Push Changes
```sh ```sh
git push origin add-new-feature git push origin add-new-feature
``` ```
### 5. Create a Merge Request ### 5. Create a Merge Request
- Go to `https://gitgud.io/YOUR_USERNAME/example-project/merge_requests` and click on "New 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`) - Choose the source branch `add-new-feature` and target branch (default: `main` or `master`)
- Fill in the details and submit the merge request - Fill in the details and submit the merge request

View file

@ -1,7 +1,7 @@
# GameUpdate patch source (copy to patch-config.txt and edit). # GameUpdate patch source (copy to patch-config.txt and edit).
# Lines starting with # are comments. Keys are case-insensitive. # 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 # Config → Game Update Defaults. Step 1 "Copy gameupdate/" writes
# those into this file automatically (you still set repo= per game). # those into this file automatically (you still set repo= per game).

View file

@ -1,23 +1,23 @@
""" """
DazedMTLTool GUI Package DazedTL GUI Package
""" """
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "DazedTranslations" __author__ = "DazedTranslations"
# Package imports # Package imports
from .main import DazedMTLGUI from .main import DazedMTLGUI
from .config_tab import ConfigTab from .config_tab import ConfigTab
from .rpgmaker_tab import RPGMakerTab from .rpgmaker_tab import RPGMakerTab
from .log_viewer import LogViewer from .log_viewer import LogViewer
from .file_manager import FileManager from .file_manager import FileManager
from .workflow_tab import WorkflowTab from .workflow_tab import WorkflowTab
__all__ = [ __all__ = [
"DazedMTLGUI", "DazedMTLGUI",
"ConfigTab", "ConfigTab",
"RPGMakerTab", "RPGMakerTab",
"LogViewer", "LogViewer",
"FileManager", "FileManager",
"WorkflowTab", "WorkflowTab",
] ]

View file

@ -1,426 +1,428 @@
""" """
File Manager - Handle input and output files File Manager - Handle input and output files
""" """
from pathlib import Path from pathlib import Path
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
QPushButton, QLabel, QFileDialog, QMessageBox, QGroupBox, QPushButton, QLabel, QFileDialog, QMessageBox, QGroupBox,
QSplitter, QTextEdit, QProgressBar, QCheckBox QSplitter, QTextEdit, QProgressBar, QCheckBox
) )
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
import json import json
from util.paths import APP_NAME, ORG_NAME
class FileManager(QWidget):
"""Widget for managing input and output files."""
class FileManager(QWidget):
files_changed = pyqtSignal() """Widget for managing input and output files."""
def __init__(self): files_changed = pyqtSignal()
super().__init__()
# Persistent settings for remembering last-open directory def __init__(self):
try: super().__init__()
self.settings = QSettings("DazedTranslations", "DazedMTLTool") # Persistent settings for remembering last-open directory
except Exception: try:
self.settings = None self.settings = QSettings(ORG_NAME, APP_NAME)
self.input_files = [] except Exception:
self.output_directory = Path("translated") self.settings = None
self.init_ui() self.input_files = []
self.refresh_files() self.output_directory = Path("translated")
self.init_ui()
def init_ui(self): self.refresh_files()
"""Initialize the user interface."""
layout = QVBoxLayout() def init_ui(self):
"""Initialize the user interface."""
# Create splitter for input and output sections layout = QVBoxLayout()
splitter = QSplitter(Qt.Horizontal)
# Create splitter for input and output sections
# Input files section splitter = QSplitter(Qt.Horizontal)
input_group = self.create_input_section()
splitter.addWidget(input_group) # Input files section
input_group = self.create_input_section()
# Output files section splitter.addWidget(input_group)
output_group = self.create_output_section()
splitter.addWidget(output_group) # Output files section
output_group = self.create_output_section()
splitter.setSizes([400, 400]) splitter.addWidget(output_group)
layout.addWidget(splitter)
splitter.setSizes([400, 400])
# Progress section layout.addWidget(splitter)
progress_group = self.create_progress_section()
layout.addWidget(progress_group) # Progress section
progress_group = self.create_progress_section()
self.setLayout(layout) layout.addWidget(progress_group)
def create_input_section(self): self.setLayout(layout)
"""Create the input files section."""
group = QGroupBox("Input Files") def create_input_section(self):
layout = QVBoxLayout() """Create the input files section."""
group = QGroupBox("Input Files")
# Buttons layout = QVBoxLayout()
button_layout = QHBoxLayout()
# Buttons
self.add_files_button = QPushButton("Add Files") button_layout = QHBoxLayout()
self.add_files_button.clicked.connect(self.add_input_files)
self.add_files_button = QPushButton("Add Files")
self.add_folder_button = QPushButton("Add Folder") self.add_files_button.clicked.connect(self.add_input_files)
self.add_folder_button.clicked.connect(self.add_input_folder)
self.add_folder_button = QPushButton("Add Folder")
self.remove_files_button = QPushButton("Remove Selected") self.add_folder_button.clicked.connect(self.add_input_folder)
self.remove_files_button.clicked.connect(self.remove_selected_files)
self.remove_files_button = QPushButton("Remove Selected")
self.clear_files_button = QPushButton("Clear All") self.remove_files_button.clicked.connect(self.remove_selected_files)
self.clear_files_button.clicked.connect(self.clear_input_files)
self.clear_files_button = QPushButton("Clear All")
button_layout.addWidget(self.add_files_button) self.clear_files_button.clicked.connect(self.clear_input_files)
button_layout.addWidget(self.add_folder_button)
button_layout.addWidget(self.remove_files_button) button_layout.addWidget(self.add_files_button)
button_layout.addWidget(self.clear_files_button) button_layout.addWidget(self.add_folder_button)
button_layout.addWidget(self.remove_files_button)
layout.addLayout(button_layout) button_layout.addWidget(self.clear_files_button)
# File tree layout.addLayout(button_layout)
self.input_tree = QTreeWidget()
self.input_tree.setHeaderLabels(["File", "Size", "Type"]) # File tree
self.input_tree.itemSelectionChanged.connect(self.on_input_selection_changed) self.input_tree = QTreeWidget()
layout.addWidget(self.input_tree) self.input_tree.setHeaderLabels(["File", "Size", "Type"])
self.input_tree.itemSelectionChanged.connect(self.on_input_selection_changed)
# File info layout.addWidget(self.input_tree)
self.input_info = QTextEdit()
self.input_info.setMaximumHeight(100) # File info
self.input_info.setReadOnly(True) self.input_info = QTextEdit()
layout.addWidget(self.input_info) self.input_info.setMaximumHeight(100)
self.input_info.setReadOnly(True)
group.setLayout(layout) layout.addWidget(self.input_info)
return group
group.setLayout(layout)
def create_output_section(self): return group
"""Create the output files section."""
group = QGroupBox("Output Files") def create_output_section(self):
layout = QVBoxLayout() """Create the output files section."""
group = QGroupBox("Output Files")
# Output directory selection layout = QVBoxLayout()
dir_layout = QHBoxLayout()
# Output directory selection
self.output_dir_label = QLabel(str(self.output_directory)) dir_layout = QHBoxLayout()
self.output_dir_label.setStyleSheet("border: 1px solid #555; padding: 5px;")
self.output_dir_label = QLabel(str(self.output_directory))
self.browse_output_button = QPushButton("Browse") self.output_dir_label.setStyleSheet("border: 1px solid #555; padding: 5px;")
self.browse_output_button.clicked.connect(self.browse_output_directory)
self.browse_output_button = QPushButton("Browse")
dir_layout.addWidget(QLabel("Output Directory:")) self.browse_output_button.clicked.connect(self.browse_output_directory)
dir_layout.addWidget(self.output_dir_label)
dir_layout.addWidget(self.browse_output_button) dir_layout.addWidget(QLabel("Output Directory:"))
dir_layout.addWidget(self.output_dir_label)
layout.addLayout(dir_layout) dir_layout.addWidget(self.browse_output_button)
# Output file tree layout.addLayout(dir_layout)
self.output_tree = QTreeWidget()
self.output_tree.setHeaderLabels(["File", "Size", "Modified"]) # Output file tree
self.output_tree.itemSelectionChanged.connect(self.on_output_selection_changed) self.output_tree = QTreeWidget()
layout.addWidget(self.output_tree) self.output_tree.setHeaderLabels(["File", "Size", "Modified"])
self.output_tree.itemSelectionChanged.connect(self.on_output_selection_changed)
# Output file preview layout.addWidget(self.output_tree)
self.output_preview = QTextEdit()
self.output_preview.setMaximumHeight(100) # Output file preview
self.output_preview.setReadOnly(True) self.output_preview = QTextEdit()
layout.addWidget(self.output_preview) self.output_preview.setMaximumHeight(100)
self.output_preview.setReadOnly(True)
# Output buttons layout.addWidget(self.output_preview)
output_button_layout = QHBoxLayout()
# Output buttons
self.open_output_button = QPushButton("Open in Editor") output_button_layout = QHBoxLayout()
self.open_output_button.clicked.connect(self.open_output_file)
self.open_output_button = QPushButton("Open in Editor")
self.delete_output_button = QPushButton("Delete Selected") self.open_output_button.clicked.connect(self.open_output_file)
self.delete_output_button.clicked.connect(self.delete_output_file)
self.delete_output_button = QPushButton("Delete Selected")
output_button_layout.addWidget(self.open_output_button) self.delete_output_button.clicked.connect(self.delete_output_file)
output_button_layout.addWidget(self.delete_output_button)
output_button_layout.addStretch() output_button_layout.addWidget(self.open_output_button)
output_button_layout.addWidget(self.delete_output_button)
layout.addLayout(output_button_layout) output_button_layout.addStretch()
group.setLayout(layout) layout.addLayout(output_button_layout)
return group
group.setLayout(layout)
def create_progress_section(self): return group
"""Create the progress monitoring section."""
group = QGroupBox("Translation Progress") def create_progress_section(self):
layout = QVBoxLayout() """Create the progress monitoring section."""
group = QGroupBox("Translation Progress")
# Progress bar layout = QVBoxLayout()
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False) # Progress bar
layout.addWidget(self.progress_bar) self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
# Status and options layout.addWidget(self.progress_bar)
status_layout = QHBoxLayout()
# Status and options
self.status_label = QLabel("Ready") status_layout = QHBoxLayout()
self.backup_cb = QCheckBox("Create Backups")
self.backup_cb.setChecked(True) self.status_label = QLabel("Ready")
self.backup_cb = QCheckBox("Create Backups")
status_layout.addWidget(self.status_label) self.backup_cb.setChecked(True)
status_layout.addStretch()
status_layout.addWidget(self.backup_cb) status_layout.addWidget(self.status_label)
status_layout.addStretch()
layout.addLayout(status_layout) status_layout.addWidget(self.backup_cb)
group.setLayout(layout) layout.addLayout(status_layout)
return group
group.setLayout(layout)
def add_input_files(self): return group
"""Add input files via dialog."""
# Try to restore last used directory def add_input_files(self):
start_dir = "" """Add input files via dialog."""
try: # Try to restore last used directory
if self.settings: start_dir = ""
start_dir = self.settings.value("last_open_dir", "") or "" try:
except Exception: if self.settings:
start_dir = "" start_dir = self.settings.value("last_open_dir", "") or ""
except Exception:
files, _ = QFileDialog.getOpenFileNames( start_dir = ""
self,
"Select Input Files", files, _ = QFileDialog.getOpenFileNames(
start_dir, self,
"JSON Files (*.json);;All Files (*)" "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) for file_path in files:
if file_path not in self.input_files:
# Persist directory used self.input_files.append(file_path)
try:
if self.settings and len(files) > 0: # Persist directory used
import os try:
self.settings.setValue("last_open_dir", os.path.dirname(files[0])) if self.settings and len(files) > 0:
except Exception: import os
pass self.settings.setValue("last_open_dir", os.path.dirname(files[0]))
except Exception:
self.refresh_input_files() pass
def add_input_folder(self): self.refresh_input_files()
"""Add all files from a folder."""
folder = QFileDialog.getExistingDirectory(self, "Select Input Folder") def add_input_folder(self):
"""Add all files from a folder."""
if folder: folder = QFileDialog.getExistingDirectory(self, "Select Input Folder")
folder_path = Path(folder)
for file_path in folder_path.rglob("*.json"): if folder:
file_str = str(file_path) folder_path = Path(folder)
if file_str not in self.input_files: for file_path in folder_path.rglob("*.json"):
self.input_files.append(file_str) file_str = str(file_path)
if file_str not in self.input_files:
self.refresh_input_files() self.input_files.append(file_str)
def remove_selected_files(self): self.refresh_input_files()
"""Remove selected input files."""
selected_items = self.input_tree.selectedItems() def remove_selected_files(self):
for item in selected_items: """Remove selected input files."""
file_path = item.text(0) selected_items = self.input_tree.selectedItems()
if file_path in self.input_files: for item in selected_items:
self.input_files.remove(file_path) file_path = item.text(0)
if file_path in self.input_files:
self.refresh_input_files() self.input_files.remove(file_path)
def clear_input_files(self): self.refresh_input_files()
"""Clear all input files."""
self.input_files.clear() def clear_input_files(self):
self.refresh_input_files() """Clear all input files."""
self.input_files.clear()
def refresh_input_files(self): self.refresh_input_files()
"""Refresh the input files display."""
self.input_tree.clear() def refresh_input_files(self):
"""Refresh the input files display."""
for file_path in self.input_files: self.input_tree.clear()
path_obj = Path(file_path)
if path_obj.exists(): for file_path in self.input_files:
size = path_obj.stat().st_size path_obj = Path(file_path)
file_type = path_obj.suffix if path_obj.exists():
size = path_obj.stat().st_size
item = QTreeWidgetItem([ file_type = path_obj.suffix
path_obj.name,
f"{size:,} bytes", item = QTreeWidgetItem([
file_type path_obj.name,
]) f"{size:,} bytes",
item.setToolTip(0, str(path_obj)) file_type
self.input_tree.addTopLevelItem(item) ])
else: item.setToolTip(0, str(path_obj))
# File doesn't exist - show in red self.input_tree.addTopLevelItem(item)
item = QTreeWidgetItem([ else:
f"{path_obj.name} (Missing)", # File doesn't exist - show in red
"0 bytes", item = QTreeWidgetItem([
"N/A" f"{path_obj.name} (Missing)",
]) "0 bytes",
item.setBackground(0, Qt.red) "N/A"
self.input_tree.addTopLevelItem(item) ])
item.setBackground(0, Qt.red)
# Update info self.input_tree.addTopLevelItem(item)
self.update_input_info()
# Update info
def refresh_output_files(self): self.update_input_info()
"""Refresh the output files display."""
self.output_tree.clear() def refresh_output_files(self):
"""Refresh the output files display."""
if self.output_directory.exists(): self.output_tree.clear()
for file_path in self.output_directory.iterdir():
if file_path.is_file(): if self.output_directory.exists():
size = file_path.stat().st_size for file_path in self.output_directory.iterdir():
modified = file_path.stat().st_mtime if file_path.is_file():
size = file_path.stat().st_size
import datetime modified = file_path.stat().st_mtime
modified_str = datetime.datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M")
import datetime
item = QTreeWidgetItem([ modified_str = datetime.datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M")
file_path.name,
f"{size:,} bytes", item = QTreeWidgetItem([
modified_str file_path.name,
]) f"{size:,} bytes",
item.setToolTip(0, str(file_path)) modified_str
self.output_tree.addTopLevelItem(item) ])
item.setToolTip(0, str(file_path))
def browse_output_directory(self): self.output_tree.addTopLevelItem(item)
"""Browse for output directory."""
directory = QFileDialog.getExistingDirectory( def browse_output_directory(self):
self, "Select Output Directory", str(self.output_directory) """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)) if directory:
self.refresh_output_files() self.output_directory = Path(directory)
self.output_dir_label.setText(str(self.output_directory))
def refresh_files(self): self.refresh_output_files()
"""Refresh both input and output file displays."""
self.refresh_input_files() def refresh_files(self):
self.refresh_output_files() """Refresh both input and output file displays."""
self.refresh_input_files()
def on_input_selection_changed(self): self.refresh_output_files()
"""Handle input file selection change."""
selected_items = self.input_tree.selectedItems() def on_input_selection_changed(self):
if selected_items: """Handle input file selection change."""
item = selected_items[0] selected_items = self.input_tree.selectedItems()
file_name = item.text(0) if selected_items:
item = selected_items[0]
# Find the full path file_name = item.text(0)
full_path = None
for file_path in self.input_files: # Find the full path
if Path(file_path).name == file_name: full_path = None
full_path = file_path for file_path in self.input_files:
break if Path(file_path).name == file_name:
full_path = file_path
if full_path and Path(full_path).exists(): break
try:
with open(full_path, 'r', encoding='utf-8') as f: if full_path and Path(full_path).exists():
content = f.read() try:
with open(full_path, 'r', encoding='utf-8') as f:
# Show preview of file structure content = f.read()
if full_path.endswith('.json'):
data = json.loads(content) # Show preview of file structure
if isinstance(data, dict): if full_path.endswith('.json'):
keys = list(data.keys())[:10] # Show first 10 keys data = json.loads(content)
preview = f"JSON Object with {len(data)} keys:\n" if isinstance(data, dict):
preview += "\n".join(keys) keys = list(data.keys())[:10] # Show first 10 keys
if len(data) > 10: preview = f"JSON Object with {len(data)} keys:\n"
preview += f"\n... and {len(data) - 10} more" preview += "\n".join(keys)
elif isinstance(data, list): if len(data) > 10:
preview = f"JSON Array with {len(data)} items" preview += f"\n... and {len(data) - 10} more"
else: elif isinstance(data, list):
preview = f"JSON: {type(data).__name__}" preview = f"JSON Array with {len(data)} items"
else: else:
preview = content[:500] + "..." if len(content) > 500 else content preview = f"JSON: {type(data).__name__}"
else:
self.input_info.setPlainText(preview) preview = content[:500] + "..." if len(content) > 500 else content
except Exception as e:
self.input_info.setPlainText(f"Error reading file: {str(e)}") self.input_info.setPlainText(preview)
except Exception as e:
def on_output_selection_changed(self): self.input_info.setPlainText(f"Error reading file: {str(e)}")
"""Handle output file selection change."""
selected_items = self.output_tree.selectedItems() def on_output_selection_changed(self):
if selected_items: """Handle output file selection change."""
item = selected_items[0] selected_items = self.output_tree.selectedItems()
file_name = item.text(0) if selected_items:
file_path = self.output_directory / file_name item = selected_items[0]
file_name = item.text(0)
if file_path.exists(): file_path = self.output_directory / file_name
try:
with open(file_path, 'r', encoding='utf-8') as f: if file_path.exists():
content = f.read() try:
with open(file_path, 'r', encoding='utf-8') as f:
preview = content[:500] + "..." if len(content) > 500 else content content = f.read()
self.output_preview.setPlainText(preview)
except Exception as e: preview = content[:500] + "..." if len(content) > 500 else content
self.output_preview.setPlainText(f"Error reading file: {str(e)}") self.output_preview.setPlainText(preview)
except Exception as e:
def open_output_file(self): self.output_preview.setPlainText(f"Error reading file: {str(e)}")
"""Open selected output file in external editor."""
selected_items = self.output_tree.selectedItems() def open_output_file(self):
if selected_items: """Open selected output file in external editor."""
item = selected_items[0] selected_items = self.output_tree.selectedItems()
file_name = item.text(0) if selected_items:
file_path = self.output_directory / file_name item = selected_items[0]
file_name = item.text(0)
if file_path.exists(): file_path = self.output_directory / file_name
import subprocess
try: if file_path.exists():
subprocess.run(['notepad.exe', str(file_path)], check=True) import subprocess
except subprocess.CalledProcessError: try:
QMessageBox.warning(self, "Warning", "Could not open file in external editor") subprocess.run(['notepad.exe', str(file_path)], check=True)
except subprocess.CalledProcessError:
def delete_output_file(self): QMessageBox.warning(self, "Warning", "Could not open file in external editor")
"""Delete selected output file."""
selected_items = self.output_tree.selectedItems() def delete_output_file(self):
if selected_items: """Delete selected output file."""
item = selected_items[0] selected_items = self.output_tree.selectedItems()
file_name = item.text(0) if selected_items:
file_path = self.output_directory / file_name item = selected_items[0]
file_name = item.text(0)
reply = QMessageBox.question( file_path = self.output_directory / file_name
self,
"Confirm Deletion", reply = QMessageBox.question(
f"Are you sure you want to delete '{file_name}'?", self,
QMessageBox.Yes | QMessageBox.No, "Confirm Deletion",
QMessageBox.No 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() if reply == QMessageBox.Yes and file_path.exists():
self.refresh_output_files() try:
except Exception as e: file_path.unlink()
QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}") self.refresh_output_files()
except Exception as e:
def update_input_info(self): QMessageBox.critical(self, "Error", f"Failed to delete file:\n{str(e)}")
"""Update the input files information."""
total_files = len(self.input_files) def update_input_info(self):
total_size = 0 """Update the input files information."""
total_files = len(self.input_files)
for file_path in self.input_files: total_size = 0
path_obj = Path(file_path)
if path_obj.exists(): for file_path in self.input_files:
total_size += path_obj.stat().st_size path_obj = Path(file_path)
if path_obj.exists():
info = f"Total Files: {total_files}\nTotal Size: {total_size:,} bytes" total_size += path_obj.stat().st_size
# Count by file type info = f"Total Files: {total_files}\nTotal Size: {total_size:,} bytes"
extensions = {}
for file_path in self.input_files: # Count by file type
ext = Path(file_path).suffix extensions = {}
extensions[ext] = extensions.get(ext, 0) + 1 for file_path in self.input_files:
ext = Path(file_path).suffix
if extensions: extensions[ext] = extensions.get(ext, 0) + 1
info += "\n\nFile Types:"
for ext, count in sorted(extensions.items()): if extensions:
info += f"\n{ext or 'No extension'}: {count}" info += "\n\nFile Types:"
for ext, count in sorted(extensions.items()):
self.input_info.setPlainText(info) info += f"\n{ext or 'No extension'}: {count}"
def get_input_files(self): self.input_info.setPlainText(info)
"""Get list of input files."""
return self.input_files.copy() def get_input_files(self):
"""Get list of input files."""
def get_output_directory(self): return self.input_files.copy()
"""Get output directory path."""
return self.output_directory def get_output_directory(self):
"""Get output directory path."""
return self.output_directory

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
DazedMTLTool GUI - Main Application DazedTL GUI - Main Application
A PyQt-based graphical user interface for the DazedMTLTool translation system. A PyQt-based graphical user interface for the DazedTL translation system.
""" """
import re 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 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: def load_application_icon() -> QIcon:
@ -56,7 +63,7 @@ def check_tool_update() -> str | None:
class BackgroundUpdateCheckThread(QThread): 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 finished = pyqtSignal(object) # str | None — pending tool SHA when available
@ -151,7 +158,7 @@ class UpdateThread(QThread):
@classmethod @classmethod
def fetch_latest_sha(cls) -> str: def fetch_latest_sha(cls) -> str:
req = urllib.request.Request( 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: with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())["commit"]["id"] return json.loads(resp.read())["commit"]["id"]
@ -167,7 +174,7 @@ class UpdateThread(QThread):
def _download_archive(self, zip_path: Path): def _download_archive(self, zip_path: Path):
req = urllib.request.Request( 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: 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) total = int(resp.headers.get("Content-Length", 0) or 0)
@ -367,7 +374,7 @@ class UpdateDialog(QDialog):
title_col = QVBoxLayout() title_col = QVBoxLayout()
title_col.setSpacing(2) title_col.setSpacing(2)
self.title_label = QLabel("DazedMTLTool Update") self.title_label = QLabel(f"{APP_NAME} Update")
self.title_label.setObjectName("updateTitle") self.title_label.setObjectName("updateTitle")
self.subtitle_label = QLabel("Stay current with the latest fixes and features.") self.subtitle_label = QLabel("Stay current with the latest fixes and features.")
self.subtitle_label.setObjectName("updateDetail") self.subtitle_label.setObjectName("updateDetail")
@ -577,7 +584,7 @@ class UpdateDialog(QDialog):
self.current_version_label.setText(sha) self.current_version_label.setText(sha)
self.headline_label.setText("Update installed successfully") self.headline_label.setText("Update installed successfully")
self.detail_label.setText( 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.") self.subtitle_label.setText("Update complete.")
if isinstance(self.parent(), DazedMTLGUI): if isinstance(self.parent(), DazedMTLGUI):
@ -620,7 +627,7 @@ from gui.skills_tab import SkillsTab
from gui.batch_tab import BatchTab from gui.batch_tab import BatchTab
class DazedMTLGUI(QMainWindow): class DazedMTLGUI(QMainWindow):
"""Main GUI window for the DazedMTLTool.""" """Main GUI window for DazedTL."""
PAGE_GUIDE = 0 PAGE_GUIDE = 0
PAGE_WORKFLOW = 1 PAGE_WORKFLOW = 1
@ -631,7 +638,7 @@ class DazedMTLGUI(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.settings = QSettings("DazedTranslations", "DazedMTLTool") self.settings = QSettings(ORG_NAME, APP_NAME)
self._pending_tool_sha: str | None = None self._pending_tool_sha: str | None = None
self._update_check_thread = None self._update_check_thread = None
self._shutdown_started = False self._shutdown_started = False
@ -791,16 +798,16 @@ class DazedMTLGUI(QMainWindow):
# Update window title only when non-default scale is active # Update window title only when non-default scale is active
if scale_factor != 1.0: 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: else:
self.setWindowTitle("DazedMTLTool - Visual Translation Interface") self.setWindowTitle(f"{APP_NAME} - Visual Translation Interface")
except Exception as e: except Exception as e:
print(f"Warning: Could not apply font scaling: {e}") print(f"Warning: Could not apply font scaling: {e}")
def init_ui(self): def init_ui(self):
"""Initialize the user interface.""" """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 # Get screen geometry and set window size more responsively
screen = QApplication.primaryScreen() screen = QApplication.primaryScreen()
@ -1012,7 +1019,7 @@ class DazedMTLGUI(QMainWindow):
return container return container
def start_background_update_check(self): 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(): if self._update_check_thread and self._update_check_thread.isRunning():
return return
self._update_check_thread = BackgroundUpdateCheckThread(self) self._update_check_thread = BackgroundUpdateCheckThread(self)
@ -1221,15 +1228,14 @@ class DazedMTLGUI(QMainWindow):
"""Show the about dialog.""" """Show the about dialog."""
QMessageBox.about( QMessageBox.about(
self, self,
"About DazedMTLTool GUI", f"About {APP_NAME}",
""" f"""
<h3>DazedMTLTool GUI</h3> <h3>{APP_NAME}</h3>
<p>A visual interface for the DazedMTLTool translation system.</p> <p>An AI translation tool for visual novels, RPG games, and other text-based content.</p>
<p>This tool helps translate visual novels, RPG games, and other text-based content using AI translation services.</p>
<p><b>Features:</b></p> <p><b>Features:</b></p>
<ul> <ul>
<li>Guided Workflow for RPG Maker and WolfDawn</li>
<li>Visual configuration management</li> <li>Visual configuration management</li>
<li>Module-specific settings</li>
<li>Real-time translation monitoring</li> <li>Real-time translation monitoring</li>
<li>File management and organization</li> <li>File management and organization</li>
</ul> </ul>
@ -1269,10 +1275,11 @@ def main():
install_qt_message_filter() install_qt_message_filter()
ensure_linux_desktop_entry() ensure_linux_desktop_entry()
QGuiApplication.setDesktopFileName("DazedMTLTool") QGuiApplication.setDesktopFileName(APP_NAME)
QCoreApplication.setOrganizationName("DazedTranslations") QCoreApplication.setOrganizationName(ORG_NAME)
QCoreApplication.setApplicationName("DazedMTLTool") QCoreApplication.setApplicationName(APP_NAME)
migrate_app_settings()
app = QApplication(sys.argv) app = QApplication(sys.argv)
@ -1283,7 +1290,7 @@ def main():
try: try:
import ctypes import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
"DazedTranslations.DazedMTLTool.1" f"{ORG_NAME}.{APP_NAME}.1"
) )
except Exception: except Exception:
pass pass

File diff suppressed because it is too large Load diff

View file

@ -101,7 +101,7 @@ from util.wolfdawn import wrap_search as wolf_ws
import util.dazedwrap as dazedwrap import util.dazedwrap as dazedwrap
from gui.setup_skills_editors import SetupSkillsEditors 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 ( from util.project_scanner import (
detect_wolf_layout, detect_wolf_layout,
find_wolf_text_archives, find_wolf_text_archives,
@ -208,7 +208,7 @@ class WolfWorkflowTab(QWidget):
super().__init__(parent) super().__init__(parent)
self.parent_window = parent self.parent_window = parent
try: try:
self.settings = QSettings("DazedTranslations", "DazedMTLTool") self.settings = QSettings(ORG_NAME, APP_NAME)
except Exception: except Exception:
self.settings = None self.settings = None
@ -3670,7 +3670,7 @@ class WolfWorkflowTab(QWidget):
self._run_task(task, on_done=_after) self._run_task(task, on_done=_after)
def _tool_root(self) -> Path: def _tool_root(self) -> Path:
"""DazedMTLTool project root (``files/``, ``translated/``, etc.).""" """DazedTL project root (``files/``, ``translated/``, etc.)."""
return Path(__file__).resolve().parent.parent return Path(__file__).resolve().parent.parent
def _translated_path(self, json_name: str) -> Path | None: def _translated_path(self, json_name: str) -> Path | None:

View file

@ -20,7 +20,7 @@ import sys
import threading import threading
from pathlib import Path 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.skills import load_project_setup
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
@ -638,7 +638,7 @@ class WorkflowTab(QWidget):
super().__init__(parent) super().__init__(parent)
self.parent_window = parent self.parent_window = parent
try: try:
self.settings = QSettings("DazedTranslations", "DazedMTLTool") self.settings = QSettings(ORG_NAME, APP_NAME)
except Exception: except Exception:
self.settings = None self.settings = None

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/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)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" || exit 1 cd "$ROOT" || exit 1

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Launch script for DazedMTLTool GUI.""" """Launch script for DazedTL GUI."""
import sys import sys
from pathlib import Path from pathlib import Path
@ -40,7 +40,7 @@ def check_dependencies():
def main(): def main():
"""Main entry point.""" """Main entry point."""
print("DazedMTLTool GUI Launcher") print("DazedTL GUI Launcher")
print("=" * 40) print("=" * 40)
from util.paths import ensure_vocab_file, migrate_root_data_files from util.paths import ensure_vocab_file, migrate_root_data_files

View file

@ -35,7 +35,7 @@ Or explicitly:
Manual equivalent (must run from project root, with venv activated): Manual equivalent (must run from project root, with venv activated):
```bash ```bash
cd /path/to/DazedMTLTool cd /path/to/DazedTL
source .venv/bin/activate # or: source venv/bin/activate source .venv/bin/activate # or: source venv/bin/activate
python -m unittest tests.test_mvmz_source_original -v python -m unittest tests.test_mvmz_source_original -v
``` ```

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/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 set -euo pipefail

View file

@ -320,7 +320,7 @@ class WolfBundledOnlyTests(unittest.TestCase):
with patch("util.wolfdawn.bundled_binary_path", return_value=missing): with patch("util.wolfdawn.bundled_binary_path", return_value=missing):
with self.assertRaises(WolfDawnError) as ctx: with self.assertRaises(WolfDawnError) as ctx:
ensure_wolf_binary() 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): def test_ensure_wolf_binary_never_downloads(self):
missing = Path("/nonexistent/wolf/missing") missing = Path("/nonexistent/wolf/missing")

View file

@ -4,7 +4,7 @@ RV2JSON.exe — https://github.com/Sinflower/RV2JSON (bin/RV2JSON.exe)
Decrypter CLI https://github.com/uuksu/RPGMakerDecrypter (release asset) Decrypter CLI https://github.com/uuksu/RPGMakerDecrypter (release asset)
End users receive curated copies via the offline bundle (``util/ace/offline/``) 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``). (``--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). # Legacy name shipped in older DazedMTLTool commits (local only, not in git).
DECRYPTER_LEGACY = ACE_DIR / "RPGMakerDecrypter.exe" DECRYPTER_LEGACY = ACE_DIR / "RPGMakerDecrypter.exe"
USER_AGENT = "DazedMTLTool" USER_AGENT = "DazedTL"
def _load_versions() -> dict: def _load_versions() -> dict:
@ -184,7 +184,7 @@ def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
if RV2JSON_LOCAL.is_file(): if RV2JSON_LOCAL.is_file():
return True return True
_log( _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.", f"to refresh util/ace/offline/RV2JSON.exe.",
log_fn, 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(): if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
return True return True
_log( _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}.", f"to refresh util/ace/offline/{DECRYPTER_ASSET}.",
log_fn, log_fn,
) )

View file

@ -1,217 +1,217 @@
"""Download / update Forge plugins from len's upstream repo (gitgud.io). """Download / update Forge plugins from len's upstream repo (gitgud.io).
Upstream: https://gitgud.io/zero64801/forge-mvmz Upstream: https://gitgud.io/zero64801/forge-mvmz
CI builds a unified forge.js plugin (master branch artifacts) for MZ. 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 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 Svelte Forge bundle. MV therefore keeps the pre-rewrite legacy plugin
(``Forge_MV.js`` / ``upstream/Forge_MV.js`` / ``legacy/Forge_MV.js``). (``Forge_MV.js`` / ``upstream/Forge_MV.js`` / ``legacy/Forge_MV.js``).
Offline copies: util/forge/upstream/ Offline copies: util/forge/upstream/
Active plugins: util/forge/Forge_MZ.js (modern), util/forge/Forge_MV.js (legacy) Active plugins: util/forge/Forge_MZ.js (modern), util/forge/Forge_MV.js (legacy)
End users receive curated copies shipped with DazedMTLTool updates. End users receive curated copies shipped with DazedTL updates.
Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``) Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``)
and only refresh the MZ modern plugin. and only refresh the MZ modern plugin.
""" """
from __future__ import annotations from __future__ import annotations
import json import json
import shutil import shutil
import sys import sys
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
_PKG_ROOT = Path(__file__).resolve().parent _PKG_ROOT = Path(__file__).resolve().parent
UPSTREAM_DIR = _PKG_ROOT / "upstream" UPSTREAM_DIR = _PKG_ROOT / "upstream"
PLUGIN_BY_ENGINE = { PLUGIN_BY_ENGINE = {
"MV": "Forge_MV", "MV": "Forge_MV",
"MZ": "Forge_MZ", "MZ": "Forge_MZ",
} }
FORGE_PROJECT = "zero64801/forge-mvmz" FORGE_PROJECT = "zero64801/forge-mvmz"
FORGE_BRANCH = "master" FORGE_BRANCH = "master"
GITGUD_API = "https://gitgud.io/api/v4" GITGUD_API = "https://gitgud.io/api/v4"
VERSION_FILE = _PKG_ROOT / ".forge_version.json" VERSION_FILE = _PKG_ROOT / ".forge_version.json"
USER_AGENT = "DazedMTLTool" USER_AGENT = "DazedTL"
def bundled_plugin_path(engine: str) -> Path: def bundled_plugin_path(engine: str) -> Path:
name = PLUGIN_BY_ENGINE.get(engine) name = PLUGIN_BY_ENGINE.get(engine)
if not name: if not name:
raise ValueError(f"Unsupported engine: {engine}") raise ValueError(f"Unsupported engine: {engine}")
return _PKG_ROOT / f"{name}.js" return _PKG_ROOT / f"{name}.js"
def upstream_plugin_path(engine: str) -> Path: def upstream_plugin_path(engine: str) -> Path:
return UPSTREAM_DIR / f"{PLUGIN_BY_ENGINE[engine]}.js" return UPSTREAM_DIR / f"{PLUGIN_BY_ENGINE[engine]}.js"
def _load_versions() -> dict: def _load_versions() -> dict:
if not VERSION_FILE.is_file(): if not VERSION_FILE.is_file():
return {} return {}
try: try:
return json.loads(VERSION_FILE.read_text(encoding="utf-8")) return json.loads(VERSION_FILE.read_text(encoding="utf-8"))
except Exception: except Exception:
return {} return {}
def _save_versions(data: dict) -> None: def _save_versions(data: dict) -> None:
VERSION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8") VERSION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
def _api_json(url: str) -> dict | list: def _api_json(url: str) -> dict | list:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=60) as resp: with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8")) return json.loads(resp.read().decode("utf-8"))
def _download(url: str, dest: Path) -> None: def _download(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True) dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part") tmp = dest.with_suffix(dest.suffix + ".part")
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=600) as resp, open(tmp, "wb") as fh: with urllib.request.urlopen(req, timeout=600) as resp, open(tmp, "wb") as fh:
shutil.copyfileobj(resp, fh) shutil.copyfileobj(resp, fh)
tmp.replace(dest) tmp.replace(dest)
def _project_id() -> str: def _project_id() -> str:
return urllib.parse.quote(FORGE_PROJECT, safe="") return urllib.parse.quote(FORGE_PROJECT, safe="")
def _upstream_commit() -> str: def _upstream_commit() -> str:
url = f"{GITGUD_API}/projects/{_project_id()}/repository/commits/{FORGE_BRANCH}" url = f"{GITGUD_API}/projects/{_project_id()}/repository/commits/{FORGE_BRANCH}"
data = _api_json(url) data = _api_json(url)
commit = data.get("id") or data.get("sha") or "" commit = data.get("id") or data.get("sha") or ""
if not commit: if not commit:
raise RuntimeError(f"Could not resolve upstream commit for {FORGE_PROJECT}") raise RuntimeError(f"Could not resolve upstream commit for {FORGE_PROJECT}")
return commit return commit
def _artifact_url(filename: str) -> str: def _artifact_url(filename: str) -> str:
"""Latest successful build_job artifact on the tracked branch.""" """Latest successful build_job artifact on the tracked branch."""
return ( return (
f"https://gitgud.io/{FORGE_PROJECT}/-/jobs/artifacts/{FORGE_BRANCH}" f"https://gitgud.io/{FORGE_PROJECT}/-/jobs/artifacts/{FORGE_BRANCH}"
f"/raw/{filename}?job=build_job" f"/raw/{filename}?job=build_job"
) )
def _log(msg: str, log_fn) -> None: def _log(msg: str, log_fn) -> None:
if log_fn: if log_fn:
log_fn(msg) log_fn(msg)
else: else:
print(msg, flush=True) print(msg, flush=True)
def _seed_from_offline(engine: str, log_fn) -> bool: def _seed_from_offline(engine: str, log_fn) -> bool:
"""Copy bundled offline copy into the active plugin path if missing.""" """Copy bundled offline copy into the active plugin path if missing."""
dest = bundled_plugin_path(engine) dest = bundled_plugin_path(engine)
if dest.is_file(): if dest.is_file():
return True return True
offline = upstream_plugin_path(engine) offline = upstream_plugin_path(engine)
if not offline.is_file(): if not offline.is_file():
return False return False
shutil.copy2(offline, dest) shutil.copy2(offline, dest)
if log_fn: if log_fn:
_log(f"Using offline bundled {dest.name}", log_fn) _log(f"Using offline bundled {dest.name}", log_fn)
return True return True
def _fetch_plugins(log_fn) -> bytes: def _fetch_plugins(log_fn) -> bytes:
"""Download the unified forge.js CI artifact.""" """Download the unified forge.js CI artifact."""
_log(f"Downloading forge.js from {FORGE_PROJECT} ({FORGE_BRANCH} CI)...", log_fn) _log(f"Downloading forge.js from {FORGE_PROJECT} ({FORGE_BRANCH} CI)...", log_fn)
return _download_bytes(_artifact_url("forge.js")) return _download_bytes(_artifact_url("forge.js"))
def _install_modern_mz_bytes(data: bytes) -> None: def _install_modern_mz_bytes(data: bytes) -> None:
"""Write modern forge.js into MZ active + offline paths only (never MV).""" """Write modern forge.js into MZ active + offline paths only (never MV)."""
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True) UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
bundled_plugin_path("MZ").write_bytes(data) bundled_plugin_path("MZ").write_bytes(data)
upstream_plugin_path("MZ").write_bytes(data) upstream_plugin_path("MZ").write_bytes(data)
def _download_bytes(url: str) -> bytes: def _download_bytes(url: str) -> bytes:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=600) as resp: with urllib.request.urlopen(req, timeout=600) as resp:
return resp.read() return resp.read()
def refresh_forge_plugins(log_fn=print) -> bool: def refresh_forge_plugins(log_fn=print) -> bool:
"""Download len's latest forge.js and refresh the MZ modern plugin only.""" """Download len's latest forge.js and refresh the MZ modern plugin only."""
try: try:
commit = _upstream_commit() commit = _upstream_commit()
except Exception as exc: except Exception as exc:
_log(f"ERROR: could not contact upstream ({exc})", log_fn) _log(f"ERROR: could not contact upstream ({exc})", log_fn)
return False return False
try: try:
data = _fetch_plugins(log_fn) data = _fetch_plugins(log_fn)
except Exception as exc: except Exception as exc:
_log(f"ERROR: download failed for forge.js ({exc})", log_fn) _log(f"ERROR: download failed for forge.js ({exc})", log_fn)
return False return False
if not data.strip().startswith(b"//"): if not data.strip().startswith(b"//"):
_log("ERROR: forge.js download did not look like a plugin file.", log_fn) _log("ERROR: forge.js download did not look like a plugin file.", log_fn)
return False return False
_install_modern_mz_bytes(data) _install_modern_mz_bytes(data)
versions = _load_versions() versions = _load_versions()
versions["commit"] = commit versions["commit"] = commit
versions["branch"] = FORGE_BRANCH versions["branch"] = FORGE_BRANCH
_save_versions(versions) _save_versions(versions)
_log( _log(
f"Forge MZ plugin updated ({commit[:12]}). " f"Forge MZ plugin updated ({commit[:12]}). "
"MV keeps the legacy Chrome-65-compatible plugin.", "MV keeps the legacy Chrome-65-compatible plugin.",
log_fn, log_fn,
) )
return True return True
def seed_forge_plugins(log_fn=None) -> None: def seed_forge_plugins(log_fn=None) -> None:
"""Copy offline-bundled Forge plugins if missing (no network).""" """Copy offline-bundled Forge plugins if missing (no network)."""
for engine in PLUGIN_BY_ENGINE: for engine in PLUGIN_BY_ENGINE:
_seed_from_offline(engine, log_fn) _seed_from_offline(engine, log_fn)
def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool: def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool:
"""Ensure Forge plugins are present from the offline bundle (no upstream fetch).""" """Ensure Forge plugins are present from the offline bundle (no upstream fetch)."""
for engine in PLUGIN_BY_ENGINE: for engine in PLUGIN_BY_ENGINE:
_seed_from_offline(engine, log_fn) _seed_from_offline(engine, log_fn)
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()] missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
if missing and not force: if missing and not force:
names = ", ".join(PLUGIN_BY_ENGINE[e] + ".js" for e in missing) names = ", ".join(PLUGIN_BY_ENGINE[e] + ".js" for e in missing)
_log( _log(
f"ERROR: Forge plugin(s) missing ({names}). " f"ERROR: Forge plugin(s) missing ({names}). "
"Update DazedMTLTool or ask the maintainer to refresh util/forge/upstream/.", "Update DazedTL or ask the maintainer to refresh util/forge/upstream/.",
log_fn, log_fn,
) )
return False return False
if force: if force:
if refresh_forge_plugins(log_fn=log_fn): if refresh_forge_plugins(log_fn=log_fn):
return True return True
have_local = all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE) have_local = all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
if not have_local: if not have_local:
_log("ERROR: Forge plugin update failed.", log_fn) _log("ERROR: Forge plugin update failed.", log_fn)
return False return False
_log("Warning: could not update Forge plugins; using local copy.", log_fn) _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) return all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
def main() -> int: def main() -> int:
if "--refresh-offline" in sys.argv: if "--refresh-offline" in sys.argv:
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True) UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
ok = refresh_forge_plugins() ok = refresh_forge_plugins()
return 0 if ok else 1 return 0 if ok else 1
force = "--force" in sys.argv or "-f" in sys.argv force = "--force" in sys.argv or "-f" in sys.argv
return 0 if ensure_forge_plugins(force=force) else 1 return 0 if ensure_forge_plugins(force=force) else 1
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())

View file

@ -82,7 +82,7 @@ def format_patch_config(
branch = (branch or "").strip() or DEFAULT_BRANCH branch = (branch or "").strip() or DEFAULT_BRANCH
lines = [ 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.", "# Edit repo= per game. forge/host/username usually stay the same.",
f"forge={forge}", f"forge={forge}",
f"host={host}", f"host={host}",

View file

@ -7,9 +7,9 @@ import subprocess
import sys import sys
from pathlib import Path 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" LAUNCH_SCRIPT = PROJECT_ROOT / "scripts" / "launch.sh"
# Qt logs this on Wayland whenever a dialog tries to steal focus (harmless noise). # 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" "[Desktop Entry]\n"
"Type=Application\n" "Type=Application\n"
f"Name={DESKTOP_ID}\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" "Comment=AI translation tool for visual novels and RPG games\n"
f"Exec={launch}\n" f"Exec={launch}\n"
f"Icon={icon}\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: 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"): if not sys.platform.startswith("linux"):
return return
if not LAUNCH_SCRIPT.is_file() or not ICON_PATH.is_file(): 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) content = _desktop_content(PROJECT_ROOT, ICON_PATH, LAUNCH_SCRIPT)
try: 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: if desktop_path.is_file() and desktop_path.read_text(encoding="utf-8") == content:
return return
desktop_path.parent.mkdir(parents=True, exist_ok=True) desktop_path.parent.mkdir(parents=True, exist_ok=True)

View file

@ -4,6 +4,11 @@ from __future__ import annotations
from pathlib import Path 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 PROJECT_ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = PROJECT_ROOT / "data" DATA_DIR = PROJECT_ROOT / "data"
VOCAB_PATH = DATA_DIR / "vocab.txt" VOCAB_PATH = DATA_DIR / "vocab.txt"
@ -55,6 +60,26 @@ def migrate_prompt_to_skills() -> None:
return 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_root_data_files()
migrate_prompt_to_skills() migrate_prompt_to_skills()

View file

@ -1,5 +1,5 @@
""" """
Shared translation utilities for DazedMTLTool. Shared translation utilities for DazedTL.
Centralized translation function used across all modules. Centralized translation function used across all modules.
""" """

View file

@ -1,10 +1,10 @@
"""WolfDawn CLI bootstrap and thin subprocess wrappers. """WolfDawn CLI bootstrap and thin subprocess wrappers.
WolfDawn is the Rust toolchain (https://gitgud.io/zero64801/wolfdawn) that 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>/`` 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 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 goes through the helpers here so command syntax and exit-code handling live in
one place. one place.
@ -215,7 +215,7 @@ _RELEASE_ASSET_MATCH = {
} }
_DOWNLOAD_UA = ( _DOWNLOAD_UA = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
"DazedMTLTool WolfDawn-fetch" "DazedTL WolfDawn-fetch"
) )
PathLike = Union[str, Path] PathLike = Union[str, Path]
@ -394,7 +394,7 @@ def ensure_wolf_binary(log_fn=print) -> Path:
return bundled return bundled
raise WolfDawnError( raise WolfDawnError(
f"No bundled WolfDawn binary for '{_platform_dir()}' at {bundled}. " 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."
) )

View file

@ -1,7 +1,7 @@
"""Check for and apply WolfDawn ``wolf`` binary updates (maintainer-only upstream fetch). """Check for and apply WolfDawn ``wolf`` binary updates (maintainer-only upstream fetch).
End users receive prebuilt binaries under ``util/wolfdawn/bin/<platform>/`` via 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 from __future__ import annotations
@ -150,7 +150,7 @@ def ensure_wolfdawn_binary(force: bool = False, log_fn=print) -> bool:
return True return True
_log( _log(
f"ERROR: no bundled WolfDawn binary for '{platform}' at {bundled}. " 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, log_fn,
) )
return False return False