diff --git a/.github/scripts/checkTranslation.py b/.github/scripts/checkTranslation.py
index 346a9ef..9d03dc6 100644
--- a/.github/scripts/checkTranslation.py
+++ b/.github/scripts/checkTranslation.py
@@ -100,7 +100,7 @@ def getScoreFromApi(fileNameToSearch: str, langId: str) -> float:
# Also handles underscore to dash conversion for Crowdin compatibility
if langApi.lower().startswith(langId.lower().replace("_", "-")):
progress = float(item["data"]["translationProgress"])
- return progress / 100
+ return progress
# Check pagination total.
total = resp["pagination"]["totalCount"]
@@ -139,9 +139,6 @@ def main():
# Default to poScore for .po and other localization files.
print(f"poScore={score}")
- # Exit with success (0) if there is at least 50% translated content.
- sys.exit(0 if score > 0.5 else 1)
-
if __name__ == "__main__":
main()
diff --git a/.github/scripts/crowdinSync.ps1 b/.github/scripts/crowdinSync.ps1
index 9b99315..8e7bb92 100644
--- a/.github/scripts/crowdinSync.ps1
+++ b/.github/scripts/crowdinSync.ps1
@@ -23,15 +23,17 @@ if (Test-Path $mdFile) {
try {
Copy-Item "$addonId.xliff" $tempXliff -Force
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
- uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
- } finally {
+ ./l10nUtil.exe md2xliff $mdFile $xliffFile -o $tempXliff
+ }
+ finally {
if (Test-Path $tempXliff) {
Remove-Item $tempXliff -Force
}
}
- } else {
+ }
+ else {
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
- uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile
+ ./l10nUtil.exe md2xliff $mdFile $xliffFile
}
}
@@ -49,8 +51,10 @@ if (Test-Path $potFile) {
if (Test-Path $xliffFile) {
Write-Host "DEBUG: Uploading updated XLIFF source to Crowdin..."
./l10nUtil.exe uploadSourceFile "$xliffFile" -c $env:L10N_UTIL_CONFIG
+
git add "$xliffFile"
git diff --staged --quiet
+
if ($LASTEXITCODE -ne 0) {
git commit -m "Update $xliffFile for $addonId"
}
@@ -69,127 +73,155 @@ New-Item -ItemType Directory -Force -Path addon/doc | Out-Null
$languageMappings = Get-Content -Raw ".github/scripts/languageMappings.json" | ConvertFrom-Json
foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
+
$langCode = $dir.Name
- if ($langCode -eq "en") { continue }
+ if ($langCode -eq "en") {
+ continue
+ }
# --- Identify codes
+
$crowdinLang = $null
- # Use the ."variable" syntax to correctly read the PSCustomObject from JSON
if ($languageMappings.PSObject.Properties.Name -contains $langCode) {
$crowdinLang = $languageMappings."$langCode"
}
- # Fallback: If no mapping is found, replace underscores with dashes for Crowdin compatibility
if (-not $crowdinLang) {
$crowdinLang = $langCode.Replace('_', '-')
}
- # The $langCode (folder name from Crowdin) represents the local repository language code.
- # It matches the NVDA directory structure, so no extra mapping is needed.
Write-Host "--- Processing Language: $langCode (Crowdin: $crowdinLang) ---" -ForegroundColor Cyan
# Paths
- $remoteMd = Join-Path $dir.FullName "$addonId.md"
+
$remoteXliff = Join-Path $dir.FullName "$addonId.xliff"
$remotePo = Join-Path $dir.FullName "$addonId.po"
+
$localMdDir = "addon/doc/$langCode"
$localMd = "$localMdDir/readme.md"
+
$localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po"
# --- 3.1 PO FILE PROCESSING ---
$poImported = $false
+ $scorePo = 0.0
+ $threshold = $env:MIN_PERCENTAGE_TRANSLATED
+
if (Test-Path $remotePo) {
- Write-Host "DEBUG: Checking Remote PO progress for $crowdinLang..."
- uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang
- if ($LASTEXITCODE -eq 0) {
- Write-Host "SUCCESS: Remote PO is valid. Importing to $localPoPath"
+
+ Write-Host "DEBUG: Evaluating Remote PO score..."
+
+ $res = uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang
+
+ $scorePo = [double](
+ ($res | Select-String "poScore=").ToString().Split("=")[1]
+ )
+
+ Write-Host "DEBUG: PO Score -> $scorePo"
+
+ if ($scorePo -ge $threshold) {
+
+ Write-Host "SUCCESS: Remote PO is above threshold. Importing to $localPoPath"
+
New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null
+
Move-Item $remotePo $localPoPath -Force
+
$poImported = $true
- } else {
- Write-Host "WARNING: Remote PO progress is below threshold."
+ }
+ else {
+
+ Write-Host "WARNING: Remote PO score is below threshold ($threshold)."
}
}
if (-not $poImported -and (Test-Path $localPoPath)) {
+
Write-Host "ACTION: Uploading local legacy PO to Crowdin ($crowdinLang) as fallback."
+
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.po" $localPoPath -c $env:L10N_UTIL_CONFIG
}
- # --- 3.2 DOCUMENTATION PROCESSING (MD & XLIFF) ---
- $scoreMd = 0.0
- $scoreXliff = 0.0
+ # --- 3.2 DOCUMENTATION PROCESSING (XLIFF ONLY) ---
- if (Test-Path $remoteMd) {
- Write-Host "DEBUG: Evaluating Remote Markdown score..."
- $res = uv run python .github/scripts/checkTranslation.py "$addonId.md" $crowdinLang
- $scoreMd = [double]($res | Select-String "mdScore=").ToString().Split("=")[1]
- } else {
- Write-Host "DEBUG: No remote Markdown file found for this language."
- }
+ $scoreXliff = 0.0
if (Test-Path $remoteXliff) {
+
Write-Host "DEBUG: Evaluating Remote XLIFF score..."
+
$res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $crowdinLang
- $scoreXliff = [double]($res | Select-String "xliffScore=").ToString().Split("=")[1]
- } else {
+
+ $scoreXliff = [double](
+ ($res | Select-String "xliffScore=").ToString().Split("=")[1]
+ )
+ }
+ else {
Write-Host "DEBUG: No remote XLIFF file found for this language."
}
- Write-Host "DEBUG: Comparison Scores -> MD: $scoreMd | XLIFF: $scoreXliff"
+ Write-Host "DEBUG: XLIFF Score -> $scoreXliff"
- $threshold = 0.5
+ $threshold = $env:MIN_PERCENTAGE_TRANSLATED
$docImported = $false
- if ($scoreXliff -gt $threshold -or $scoreMd -gt $threshold) {
- if (!(Test-Path $localMdDir)) { New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null }
-
- if ($scoreXliff -ge $scoreMd) {
- Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($langCode)..."
- ./l10nUtil.exe xliff2md $remoteXliff $localMd
- $docImported = $true
- } else {
- Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($langCode)..."
- Move-Item $remoteMd $localMd -Force
- $docImported = $true
+ if ($scoreXliff -ge $threshold) {
+
+ if (!(Test-Path $localMdDir)) {
+ New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null
}
- } else {
- Write-Host "WARNING: Both remote MD and XLIFF scores are below threshold ($threshold)."
+
+ Write-Host "SUCCESS: Importing documentation from XLIFF ($langCode)..."
+
+ ./l10nUtil.exe xliff2md $remoteXliff $localMd
+
+ $docImported = $true
}
+ else {
- if (-not $docImported -and (Test-Path $localMd)) {
- Write-Host "ACTION: Documentation quality too low. Uploading local MD to Crowdin ($crowdinLang) as fallback."
- ./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.md" $localMd -c $env:L10N_UTIL_CONFIG
+ Write-Host "WARNING: Remote XLIFF score is below threshold ($threshold)."
}
+
+ # No Markdown fallback upload anymore.
+ # XLIFF is now the single translation source in Crowdin.
}
# --- STEP 4: COMMIT UPDATED TRANSLATIONS ---
git add addon/locale addon/doc
+
git diff --staged --quiet
+
if ($LASTEXITCODE -ne 0) {
+
git commit -m "Update translations for $addonId from Crowdin (Automatic Sync)"
+
Write-Host "SUCCESS: Translations committed."
-} else {
+}
+else {
+
Write-Host "DEBUG: No changes in translations to commit."
}
# Push all generated commits after successful Crowdin synchronization
+
$pushOutput = git push 2>&1
-# Get the full repository name in "owner/repository" format (e.g., hkatic/clock)
$repository = $env:GITHUB_REPOSITORY
Write-Host $pushOutput
if ($LASTEXITCODE -ne 0) {
+
Write-Host "ERROR: Failed to push commits to $repository."
}
elseif ($pushOutput -match "Everything up-to-date") {
+
Write-Host "INFO: No new commits needed to be pushed."
}
else {
+
Write-Host "SUCCESS: New commits successfully pushed to $repository."
}
diff --git a/.github/scripts/langCodes.py b/.github/scripts/langCodes.py
deleted file mode 100644
index 044e5b5..0000000
--- a/.github/scripts/langCodes.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import sys
-
-# Mapping between Crowdin language IDs (keys) and standard NVDA directory names (values).
-# This dictionary acts as the symmetrical counterpart to 'languageMappings.json' implemented by @nvdaes.
-# It ensures that translations exported from Crowdin are stored in the correct
-# local paths (e.g., 'es-ES' from Crowdin goes into the 'es' folder).
-CROWDIN_TO_NVDA = {
- # Arabic variants
- "ar-SA": "ar_SA",
-
- # Spanish variants
- "es-ES": "es",
- "es-CO": "es_CO",
-
- # Portuguese variants
- "pt-BR": "pt_BR",
- "pt-PT": "pt_PT",
-
- # Chinese variants
- "zh-CN": "zh_CN",
- "zh-HK": "zh_HK",
- "zh-TW": "zh_TW",
-
- # Other specific mappings from the NVDA ecosystem
- "af": "af_ZA",
- "de-CH": "de_CH",
- "nb": "nb_NO",
- "nn-NO": "nn_NO",
- "sr-CS": "sr"
-}
-
-def get_nvda_code(crowdin_code):
- """
- Returns the appropriate local directory name for a given Crowdin language ID.
-
- Args:
- crowdin_code (str): The language identifier from Crowdin (e.g., 'pt-BR', 'fr').
-
- Returns:
- str: The corresponding NVDA locale folder name (e.g., 'pt_BR', 'fr').
- """
- # 1. Direct check in our verified map (Priority)
- if crowdin_code in CROWDIN_TO_NVDA:
- return CROWDIN_TO_NVDA[crowdin_code]
-
- # 2. Automated conversion for regional variants: Crowdin "xx-YY" -> NVDA "xx_YY"
- # This handles regional codes not explicitly defined in the map.
- if "-" in crowdin_code:
- return crowdin_code.replace("-", "_")
-
- # 3. Default: Return as is.
- # This covers base languages that don't use regional folders in NVDA
- # (e.g., 'fr', 'tr', 'bg', 'fi', 'fa').
- return crowdin_code
-
-if __name__ == "__main__":
- # Ensure a language code was provided as a command-line argument
- if len(sys.argv) > 1:
- # Standardize input and output the mapped code for PowerShell to capture
- print(get_nvda_code(sys.argv[1]))
\ No newline at end of file
diff --git a/.github/scripts/markdownTranslate.py b/.github/scripts/markdownTranslate.py
deleted file mode 100644
index 165e940..0000000
--- a/.github/scripts/markdownTranslate.py
+++ /dev/null
@@ -1,881 +0,0 @@
-# A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2024-2026 NV Access Limited.
-# This file is covered by the GNU General Public License.
-# See the file COPYING for more details.
-
-from collections.abc import Generator
-from collections.abc import Iterable
-import tempfile
-import os
-import contextlib
-import lxml.etree
-import argparse
-import uuid
-import re
-from itertools import zip_longest
-from xml.sax.saxutils import escape as xmlEscape
-import difflib
-from dataclasses import dataclass
-import subprocess
-
-
-def getGithubRepoURL() -> str | None:
- """
- Get the GitHub repository URL from git remote origin.
- return: The raw GitHub URL for the repository, or None if it cannot be determined.
- """
- result = subprocess.run(
- ["git", "remote", "get-url", "origin"],
- capture_output=True,
- text=True,
- check=True,
- )
- remote_url = result.stdout.strip()
- # Convert SSH or HTTPS URL to raw GitHub URL format
- if match := re.match(r"git@github\.com:(.+?)(?:\.git)?$", remote_url):
- repo_path = match.group(1)
- elif match := re.match(r"https://github\.com/(.+?)(?:\.git)?$", remote_url):
- repo_path = match.group(1)
- else:
- raise ValueError(f"Cannot parse GitHub URL from git remote: {remote_url}")
- return f"https://raw.githubusercontent.com/{repo_path}"
-
-
-re_kcTitle = re.compile(r"^()$")
-re_kcSettingsSection = re.compile(r"^()$")
-# Comments that span a single line in their entirety
-re_comment = re.compile(r"^$")
-re_heading = re.compile(r"^(#+\s+)(.+?)((?:\s+\{#.+\})?)$")
-re_bullet = re.compile(r"^(\s*\*\s+)(.+)$")
-re_number = re.compile(r"^(\s*[0-9]+\.\s+)(.+)$")
-re_hiddenHeaderRow = re.compile(r"^\|\s*\.\s*\{\.hideHeaderRow\}\s*(\|\s*\.\s*)*\|$")
-re_postTableHeaderLine = re.compile(r"^(\|\s*-+\s*)+\|$")
-re_tableRow = re.compile(r"^(\|)(.+)(\|)$")
-re_translationID = re.compile(r"^(.*)\$\(ID:([0-9a-f-]+)\)(.*)$")
-re_inlineMarkdownLintComment = re.compile(r"^(.*?)(?:\s*)(\s*)$")
-
-
-def prettyPathString(path: str) -> str:
- cwd = os.getcwd()
- if os.path.normcase(os.path.splitdrive(path)[0]) != os.path.normcase(
- os.path.splitdrive(cwd)[0],
- ):
- return path
- return os.path.relpath(path, cwd)
-
-
-@contextlib.contextmanager
-def createAndDeleteTempFilePath_contextManager(
- dir: str | None = None,
- prefix: str | None = None,
- suffix: str | None = None,
-) -> Generator[str, None, None]:
- """A context manager that creates a temporary file and deletes it when the context is exited"""
- with tempfile.NamedTemporaryFile(
- dir=dir,
- prefix=prefix,
- suffix=suffix,
- delete=False,
- ) as tempFile:
- tempFilePath = tempFile.name
- tempFile.close()
- yield tempFilePath
- os.remove(tempFilePath)
-
-
-def getLastCommitID(filePath: str) -> str:
- # Run the git log command to get the last commit ID for the given file
- result = subprocess.run(
- ["git", "log", "-n", "1", "--pretty=format:%H", "--", filePath],
- capture_output=True,
- text=True,
- check=True,
- )
- commitID = result.stdout.strip()
- if not re.match(r"[0-9a-f]{40}", commitID):
- raise ValueError(f"Invalid commit ID: '{commitID}' for file '{filePath}'")
- return commitID
-
-
-def getGitDir() -> str:
- # Run the git rev-parse command to get the root of the git directory
- result = subprocess.run(
- ["git", "rev-parse", "--show-toplevel"],
- capture_output=True,
- text=True,
- check=True,
- )
- gitDir = result.stdout.strip()
- if not os.path.isdir(gitDir):
- raise ValueError(f"Invalid git directory: '{gitDir}'")
- return gitDir
-
-
-def getRawGithubURLForPath(filePath: str) -> str:
- gitDirPath = getGitDir()
- commitID = getLastCommitID(filePath)
- relativePath = os.path.relpath(os.path.abspath(filePath), gitDirPath)
- relativePath = relativePath.replace("\\", "/")
- rawGithubRepoUrl = getGithubRepoURL()
- return f"{rawGithubRepoUrl}/{commitID}/{relativePath}"
-
-
-def preprocessMarkdownLines(mdLines: Iterable[str]) -> Iterable[str]:
- """
- Preprocess markdown lines such as removing inline markdown lint comments.\
- :param mdLines: The markdown lines to preprocess
- :returns: The preprocessed markdown lines
- """
- for mdLine in mdLines:
- # #18982: Remove markdown lint comments completely - not needed for intermediate markdown or final html.
- mdLine = re_inlineMarkdownLintComment.sub(r"\1\2", mdLine)
- yield mdLine
-
-
-def skeletonizeLine(mdLine: str) -> str | None:
- prefix = ""
- suffix = ""
- if (
- mdLine.isspace()
- or mdLine.strip() == "[TOC]"
- or re_hiddenHeaderRow.match(mdLine)
- or re_postTableHeaderLine.match(mdLine)
- ):
- return None
- elif m := re_heading.match(mdLine):
- prefix, content, suffix = m.groups()
- elif m := re_bullet.match(mdLine):
- prefix, content = m.groups()
- elif m := re_number.match(mdLine):
- prefix, content = m.groups()
- elif m := re_tableRow.match(mdLine):
- prefix, content, suffix = m.groups()
- elif m := re_kcTitle.match(mdLine):
- prefix, content, suffix = m.groups()
- elif m := re_kcSettingsSection.match(mdLine):
- prefix, content, suffix = m.groups()
- elif re_comment.match(mdLine):
- return None
- ID = str(uuid.uuid4())
- return f"{prefix}$(ID:{ID}){suffix}\n"
-
-
-@dataclass
-class Result_generateSkeleton:
- numTotalLines: int = 0
- numTranslationPlaceholders: int = 0
-
-
-def generateSkeleton(mdPath: str, outputPath: str) -> Result_generateSkeleton:
- print(
- f"Generating skeleton file {prettyPathString(outputPath)} from {prettyPathString(mdPath)}...",
- )
- res = Result_generateSkeleton()
- with (
- open(mdPath, "r", encoding="utf8") as mdFile,
- open(outputPath, "w", encoding="utf8", newline="") as outputFile,
- ):
- for mdLine in preprocessMarkdownLines(mdFile.readlines()):
- res.numTotalLines += 1
- skelLine = skeletonizeLine(mdLine)
- if skelLine:
- res.numTranslationPlaceholders += 1
- else:
- skelLine = mdLine
- outputFile.write(skelLine)
- print(
- f"Generated skeleton file with {res.numTotalLines} total lines and {res.numTranslationPlaceholders} translation placeholders",
- )
- return res
-
-
-@dataclass
-class Result_updateSkeleton:
- numAddedLines: int = 0
- numAddedTranslationPlaceholders: int = 0
- numRemovedLines: int = 0
- numRemovedTranslationPlaceholders: int = 0
- numUnchangedLines: int = 0
- numUnchangedTranslationPlaceholders: int = 0
-
-
-def extractSkeleton(xliffPath: str, outputPath: str):
- print(
- f"Extracting skeleton from {prettyPathString(xliffPath)} to {prettyPathString(outputPath)}...",
- )
- with contextlib.ExitStack() as stack:
- outputFile = stack.enter_context(
- open(outputPath, "w", encoding="utf8", newline=""),
- )
- xliff = lxml.etree.parse(xliffPath)
- xliffRoot = xliff.getroot()
- namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"}
- if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff":
- raise ValueError("Not an xliff file")
- skeletonNode = xliffRoot.find(
- "./xliff:file/xliff:skeleton",
- namespaces=namespace,
- )
- if skeletonNode is None:
- raise ValueError("No skeleton found in xliff file")
- skeletonContent = skeletonNode.text.strip()
- outputFile.write(skeletonContent)
- print(f"Extracted skeleton to {prettyPathString(outputPath)}")
-
-
-def updateSkeleton(
- origMdPath: str,
- newMdPath: str,
- origSkelPath: str,
- outputPath: str,
-) -> Result_updateSkeleton:
- print(
- f"Creating updated skeleton file {prettyPathString(outputPath)} from {prettyPathString(origSkelPath)} with changes from {prettyPathString(origMdPath)} to {prettyPathString(newMdPath)}...",
- )
- res = Result_updateSkeleton()
- with contextlib.ExitStack() as stack:
- origMdFile = stack.enter_context(open(origMdPath, "r", encoding="utf8"))
- newMdFile = stack.enter_context(open(newMdPath, "r", encoding="utf8"))
- origSkelFile = stack.enter_context(open(origSkelPath, "r", encoding="utf8"))
- outputFile = stack.enter_context(
- open(outputPath, "w", encoding="utf8", newline=""),
- )
- origMdLines = preprocessMarkdownLines(origMdFile.readlines())
- newMdLines = preprocessMarkdownLines(newMdFile.readlines())
- mdDiff = difflib.ndiff(list(origMdLines), list(newMdLines))
- origSkelLines = iter(origSkelFile.readlines())
- for mdDiffLine in mdDiff:
- if mdDiffLine.startswith("?"):
- continue
- if mdDiffLine.startswith(" "):
- res.numUnchangedLines += 1
- skelLine = next(origSkelLines)
- if re_translationID.match(skelLine):
- res.numUnchangedTranslationPlaceholders += 1
- outputFile.write(skelLine)
- elif mdDiffLine.startswith("+"):
- res.numAddedLines += 1
- skelLine = skeletonizeLine(mdDiffLine[2:])
- if skelLine:
- res.numAddedTranslationPlaceholders += 1
- else:
- skelLine = mdDiffLine[2:]
- outputFile.write(skelLine)
- elif mdDiffLine.startswith("-"):
- res.numRemovedLines += 1
- origSkelLine = next(origSkelLines)
- if re_translationID.match(origSkelLine):
- res.numRemovedTranslationPlaceholders += 1
- else:
- raise ValueError(f"Unexpected diff line: {mdDiffLine}")
- print(
- f"Updated skeleton file with {res.numAddedLines} added lines "
- f"({res.numAddedTranslationPlaceholders} translation placeholders), "
- f"{res.numRemovedLines} removed lines ({res.numRemovedTranslationPlaceholders} translation placeholders), "
- f"and {res.numUnchangedLines} unchanged lines ({res.numUnchangedTranslationPlaceholders} translation placeholders)",
- )
- return res
-
-
-@dataclass
-class Result_generateXliff:
- numTranslatableStrings: int = 0
-
-
-def generateXliff(
- mdPath: str,
- outputPath: str,
- skelPath: str | None = None,
-) -> Result_generateXliff:
- # If a skeleton file is not provided, first generate one
- with contextlib.ExitStack() as stack:
- if not skelPath:
- skelPath = stack.enter_context(
- createAndDeleteTempFilePath_contextManager(
- dir=os.path.dirname(outputPath),
- prefix=os.path.basename(mdPath),
- suffix=".skel",
- ),
- )
- generateSkeleton(mdPath=mdPath, outputPath=skelPath)
- with open(skelPath, "r", encoding="utf8") as skelFile:
- skelContent = skelFile.read()
- res = Result_generateXliff()
- print(
- f"Generating xliff file {prettyPathString(outputPath)} from {prettyPathString(mdPath)} and {prettyPathString(skelPath)}...",
- )
- with contextlib.ExitStack() as stack:
- mdFile = stack.enter_context(open(mdPath, "r", encoding="utf8"))
- outputFile = stack.enter_context(
- open(outputPath, "w", encoding="utf8", newline=""),
- )
- fileID = os.path.basename(mdPath)
- mdUri = getRawGithubURLForPath(mdPath)
- print(f"Including Github raw URL: {mdUri}")
- outputFile.write(
- '\n'
- f'\n'
- f'\n',
- )
- outputFile.write(f"\n{xmlEscape(skelContent)}\n\n")
- res.numTranslatableStrings = 0
- for lineNo, (mdLine, skelLine) in enumerate(
- zip_longest(
- preprocessMarkdownLines(mdFile.readlines()),
- skelContent.splitlines(keepends=True),
- ),
- start=1,
- ):
- mdLine = mdLine.rstrip()
- skelLine = skelLine.rstrip()
- if m := re_translationID.match(skelLine):
- res.numTranslatableStrings += 1
- prefix, ID, suffix = m.groups()
- if prefix and not mdLine.startswith(prefix):
- raise ValueError(
- f'Line {lineNo}: does not start with "{prefix}", {mdLine=}, {skelLine=}',
- )
- if suffix and not mdLine.endswith(suffix):
- raise ValueError(
- f'Line {lineNo}: does not end with "{suffix}", {mdLine=}, {skelLine=}',
- )
- source = mdLine[len(prefix) : len(mdLine) - len(suffix)]
- outputFile.write(
- f'\n\nline: {lineNo + 1}\n',
- )
- if prefix:
- outputFile.write(
- f'prefix: {xmlEscape(prefix)}\n',
- )
- if suffix:
- outputFile.write(
- f'suffix: {xmlEscape(suffix)}\n',
- )
- outputFile.write(
- "\n"
- f"\n"
- f"{xmlEscape(source)}\n"
- "\n"
- "\n", # fmt: skip
- )
- else:
- if mdLine != skelLine:
- raise ValueError(
- f"Line {lineNo}: {mdLine=} does not match {skelLine=}",
- )
- outputFile.write("\n")
- print(
- f"Generated xliff file with {res.numTranslatableStrings} translatable strings",
- )
- return res
-
-
-@dataclass
-class Result_translateXliff:
- numTranslatedStrings: int = 0
-
-
-def updateXliff(
- xliffPath: str,
- mdPath: str,
- outputPath: str,
-):
- # uses generateMarkdown, extractSkeleton, updateSkeleton, and generateXliff to generate an updated xliff file.
- outputDir = os.path.dirname(outputPath)
- print(
- f"Generating updated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} and {prettyPathString(mdPath)}...",
- )
- with contextlib.ExitStack() as stack:
- origMdPath = stack.enter_context(
- createAndDeleteTempFilePath_contextManager(
- dir=outputDir,
- prefix="generated_",
- suffix=".md",
- ),
- )
- generateMarkdown(xliffPath=xliffPath, outputPath=origMdPath, translated=False)
- origSkelPath = stack.enter_context(
- createAndDeleteTempFilePath_contextManager(
- dir=outputDir,
- prefix="extracted_",
- suffix=".skel",
- ),
- )
- extractSkeleton(xliffPath=xliffPath, outputPath=origSkelPath)
- updatedSkelPath = stack.enter_context(
- createAndDeleteTempFilePath_contextManager(
- dir=outputDir,
- prefix="updated_",
- suffix=".skel",
- ),
- )
- updateSkeleton(
- origMdPath=origMdPath,
- newMdPath=mdPath,
- origSkelPath=origSkelPath,
- outputPath=updatedSkelPath,
- )
- generateXliff(
- mdPath=mdPath,
- skelPath=updatedSkelPath,
- outputPath=outputPath,
- )
- print(f"Generated updated xliff file {prettyPathString(outputPath)}")
-
-
-def translateXliff(
- xliffPath: str,
- lang: str,
- pretranslatedMdPath: str,
- outputPath: str,
- allowBadAnchors: bool = False,
-) -> Result_translateXliff:
- print(
- f"Creating {lang} translated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} using {prettyPathString(pretranslatedMdPath)}...",
- )
- res = Result_translateXliff()
- with contextlib.ExitStack() as stack:
- pretranslatedMdFile = stack.enter_context(
- open(pretranslatedMdPath, "r", encoding="utf8"),
- )
- xliff = lxml.etree.parse(xliffPath)
- xliffRoot = xliff.getroot()
- namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"}
- if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff":
- raise ValueError("Not an xliff file")
- xliffRoot.set("trgLang", lang)
- skeletonNode = xliffRoot.find(
- "./xliff:file/xliff:skeleton",
- namespaces=namespace,
- )
- if skeletonNode is None:
- raise ValueError("No skeleton found in xliff file")
- skeletonContent = skeletonNode.text.strip()
- for lineNo, (skelLine, pretranslatedLine) in enumerate(
- zip_longest(
- skeletonContent.splitlines(),
- preprocessMarkdownLines(pretranslatedMdFile.readlines()),
- ),
- start=1,
- ):
- skelLine = skelLine.rstrip()
- pretranslatedLine = pretranslatedLine.rstrip()
- if m := re_translationID.match(skelLine):
- prefix, ID, suffix = m.groups()
- if prefix and not pretranslatedLine.startswith(prefix):
- raise ValueError(
- f'Line {lineNo} of translation does not start with "{prefix}", {pretranslatedLine=}, {skelLine=}',
- )
- if suffix and not pretranslatedLine.endswith(suffix):
- if allowBadAnchors and (m := re_heading.match(pretranslatedLine)):
- print(
- f"Warning: ignoring bad anchor in line {lineNo}: {pretranslatedLine}",
- )
- suffix = m.group(3)
- if suffix and not pretranslatedLine.endswith(suffix):
- raise ValueError(
- f'Line {lineNo} of translation: does not end with "{suffix}", {pretranslatedLine=}, {skelLine=}',
- )
- translation = pretranslatedLine[len(prefix) : len(pretranslatedLine) - len(suffix)]
- try:
- unit = xliffRoot.find(
- f'./xliff:file/xliff:unit[@id="{ID}"]',
- namespaces=namespace,
- )
- if unit is not None:
- segment = unit.find("./xliff:segment", namespaces=namespace)
- if segment is not None:
- target = lxml.etree.Element("target")
- target.text = translation
- target.tail = "\n"
- segment.append(target)
- res.numTranslatedStrings += 1
- else:
- raise ValueError(f"No segment found for unit {ID}")
- else:
- raise ValueError(f"Cannot locate Unit {ID} in xliff file")
- except Exception as e:
- e.add_note(f"Line {lineNo}: {pretranslatedLine=}, {skelLine=}")
- raise
- elif skelLine != pretranslatedLine:
- raise ValueError(
- f"Line {lineNo}: pretranslated line {pretranslatedLine!r}, does not match skeleton line {skelLine!r}",
- )
- xliff.write(outputPath, encoding="utf8", xml_declaration=True)
- print(
- f"Translated xliff file with {res.numTranslatedStrings} translated strings",
- )
- return res
-
-
-@dataclass
-class Result_generateMarkdown:
- numTotalLines = 0
- numTranslatableStrings = 0
- numTranslatedStrings = 0
- numBadTranslationStrings = 0
-
-
-def generateMarkdown(
- xliffPath: str,
- outputPath: str,
- translated: bool = True,
-) -> Result_generateMarkdown:
- print(
- f"Generating markdown file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)}...",
- )
- res = Result_generateMarkdown()
- with contextlib.ExitStack() as stack:
- outputFile = stack.enter_context(
- open(outputPath, "w", encoding="utf8", newline=""),
- )
- xliff = lxml.etree.parse(xliffPath)
- xliffRoot = xliff.getroot()
- namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"}
- if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff":
- raise ValueError("Not an xliff file")
- skeletonNode = xliffRoot.find(
- "./xliff:file/xliff:skeleton",
- namespaces=namespace,
- )
- if skeletonNode is None:
- raise ValueError("No skeleton found in xliff file")
- skeletonContent = skeletonNode.text.strip()
- for lineNum, line in enumerate(skeletonContent.splitlines(keepends=True), 1):
- res.numTotalLines += 1
- if m := re_translationID.match(line):
- prefix, ID, suffix = m.groups()
- res.numTranslatableStrings += 1
- unit = xliffRoot.find(
- f'./xliff:file/xliff:unit[@id="{ID}"]',
- namespaces=namespace,
- )
- if unit is None:
- raise ValueError(f"Cannot locate Unit {ID} in xliff file")
- segment = unit.find("./xliff:segment", namespaces=namespace)
- if segment is None:
- raise ValueError(f"No segment found for unit {ID}")
- source = segment.find("./xliff:source", namespaces=namespace)
- if source is None:
- raise ValueError(f"No source found for unit {ID}")
- translation = ""
- if translated:
- target = segment.find("./xliff:target", namespaces=namespace)
- if target is not None:
- targetText = target.text
- if targetText:
- translation = targetText
- # Crowdin treats empty targets () as a literal translation.
- # Filter out such strings and count them as bad translations.
- if translation in (
- "",
- "<target/>",
- "",
- "<target></target>",
- ):
- res.numBadTranslationStrings += 1
- translation = ""
- else:
- res.numTranslatedStrings += 1
- # If we have no translation, use the source text
- if not translation:
- sourceText = source.text
- if sourceText is None:
- raise ValueError(f"No source text found for unit {ID}")
- translation = sourceText
- outputFile.write(f"{prefix}{translation}{suffix}\n")
- else:
- outputFile.write(line)
- print(
- f"Generated markdown file with {res.numTotalLines} total lines, {res.numTranslatableStrings} translatable strings, and {res.numTranslatedStrings} translated strings. Ignoring {res.numBadTranslationStrings} bad translated strings",
- )
- return res
-
-
-def ensureMarkdownFilesMatch(path1: str, path2: str, allowBadAnchors: bool = False):
- print(
- f"Ensuring files {prettyPathString(path1)} and {prettyPathString(path2)} match...",
- )
- with contextlib.ExitStack() as stack:
- file1 = stack.enter_context(open(path1, "r", encoding="utf8"))
- file2 = stack.enter_context(open(path2, "r", encoding="utf8"))
- for lineNo, (line1, line2) in enumerate(
- zip_longest(
- preprocessMarkdownLines(file1.readlines()),
- preprocessMarkdownLines(file2.readlines()),
- ),
- start=1,
- ):
- line1 = line1.rstrip()
- line2 = line2.rstrip()
- if line1 != line2:
- if (
- re_postTableHeaderLine.match(line1)
- and re_postTableHeaderLine.match(line2)
- and line1.count("|") == line2.count("|")
- ):
- print(
- f"Warning: ignoring cell padding of post table header line at line {lineNo}: {line1}, {line2}",
- )
- continue
- if (
- re_hiddenHeaderRow.match(line1)
- and re_hiddenHeaderRow.match(line2)
- and line1.count("|") == line2.count("|")
- ):
- print(
- f"Warning: ignoring cell padding of hidden header row at line {lineNo}: {line1}, {line2}",
- )
- continue
- if allowBadAnchors and (m1 := re_heading.match(line1)) and (m2 := re_heading.match(line2)):
- print(
- f"Warning: ignoring bad anchor in headings at line {lineNo}: {line1}, {line2}",
- )
- line1 = m1.group(1) + m1.group(2)
- line2 = m2.group(1) + m2.group(2)
- if line1 != line2:
- raise ValueError(
- f"Files do not match at line {lineNo}: {line1=} {line2=}",
- )
- print("Files match")
-
-
-def markdownTranslateCommand(command: str, *args):
- print(f"Running markdownTranslate command: {command} {' '.join(args)}")
- subprocess.run(["python", __file__, command, *args], check=True)
-
-
-def pretranslateAllPossibleLanguages(langsDir: str, mdBaseName: str):
- # This function walks through all language directories in the given directory, skipping en (English) and translates the English xlif and skel file along with the lang's pretranslated md file
- enXliffPath = os.path.join(langsDir, "en", f"{mdBaseName}.xliff")
- if not os.path.exists(enXliffPath):
- raise ValueError(f"English xliff file {enXliffPath} does not exist")
- allLangs = set()
- succeededLangs = set()
- skippedLangs = set()
- for langDir in os.listdir(langsDir):
- if langDir == "en":
- continue
- langDirPath = os.path.join(langsDir, langDir)
- if not os.path.isdir(langDirPath):
- continue
- langPretranslatedMdPath = os.path.join(langDirPath, f"{mdBaseName}.md")
- if not os.path.exists(langPretranslatedMdPath):
- continue
- allLangs.add(langDir)
- langXliffPath = os.path.join(langDirPath, f"{mdBaseName}.xliff")
- if os.path.exists(langXliffPath):
- print(f"Skipping {langDir} as the xliff file already exists")
- skippedLangs.add(langDir)
- continue
- try:
- translateXliff(
- xliffPath=enXliffPath,
- lang=langDir,
- pretranslatedMdPath=langPretranslatedMdPath,
- outputPath=langXliffPath,
- allowBadAnchors=True,
- )
- except Exception as e:
- print(f"Failed to translate {langDir}: {e}")
- continue
- rebuiltLangMdPath = os.path.join(langDirPath, f"rebuilt_{mdBaseName}.md")
- try:
- generateMarkdown(
- xliffPath=langXliffPath,
- outputPath=rebuiltLangMdPath,
- )
- except Exception as e:
- print(f"Failed to rebuild {langDir} markdown: {e}")
- os.remove(langXliffPath)
- continue
- try:
- ensureMarkdownFilesMatch(
- rebuiltLangMdPath,
- langPretranslatedMdPath,
- allowBadAnchors=True,
- )
- except Exception as e:
- print(
- f"Rebuilt {langDir} markdown does not match pretranslated markdown: {e}",
- )
- os.remove(langXliffPath)
- continue
- os.remove(rebuiltLangMdPath)
- print(f"Successfully pretranslated {langDir}")
- succeededLangs.add(langDir)
- if len(skippedLangs) > 0:
- print(f"Skipped {len(skippedLangs)} languages already pretranslated.")
- print(
- f"Pretranslated {len(succeededLangs)} out of {len(allLangs) - len(skippedLangs)} languages.",
- )
-
-
-if __name__ == "__main__":
- mainParser = argparse.ArgumentParser()
- commandParser = mainParser.add_subparsers(
- title="commands",
- dest="command",
- required=True,
- )
- generateXliffParser = commandParser.add_parser("generateXliff")
- generateXliffParser.add_argument(
- "-m",
- "--markdown",
- dest="md",
- type=str,
- required=True,
- help="The markdown file to generate the xliff file for",
- )
- generateXliffParser.add_argument(
- "-o",
- "--output",
- dest="output",
- type=str,
- required=True,
- help="The file to output the xliff file to",
- )
- updateXliffParser = commandParser.add_parser("updateXliff")
- updateXliffParser.add_argument(
- "-x",
- "--xliff",
- dest="xliff",
- type=str,
- required=True,
- help="The original xliff file",
- )
- updateXliffParser.add_argument(
- "-m",
- "--newMarkdown",
- dest="md",
- type=str,
- required=True,
- help="The new markdown file",
- )
- updateXliffParser.add_argument(
- "-o",
- "--output",
- dest="output",
- type=str,
- required=True,
- help="The file to output the updated xliff to",
- )
- translateXliffParser = commandParser.add_parser("translateXliff")
- translateXliffParser.add_argument(
- "-x",
- "--xliff",
- dest="xliff",
- type=str,
- required=True,
- help="The xliff file to translate",
- )
- translateXliffParser.add_argument(
- "-l",
- "--lang",
- dest="lang",
- type=str,
- required=True,
- help="The language to translate to",
- )
- translateXliffParser.add_argument(
- "-p",
- "--pretranslatedMarkdown",
- dest="pretranslatedMd",
- type=str,
- required=True,
- help="The pretranslated markdown file to use",
- )
- translateXliffParser.add_argument(
- "-o",
- "--output",
- dest="output",
- type=str,
- required=True,
- help="The file to output the translated xliff file to",
- )
- generateMarkdownParser = commandParser.add_parser("generateMarkdown")
- generateMarkdownParser.add_argument(
- "-x",
- "--xliff",
- dest="xliff",
- type=str,
- required=True,
- help="The xliff file to generate the markdown file for",
- )
- generateMarkdownParser.add_argument(
- "-o",
- "--output",
- dest="output",
- type=str,
- required=True,
- help="The file to output the markdown file to",
- )
- generateMarkdownParser.add_argument(
- "-u",
- "--untranslated",
- dest="translated",
- action="store_false",
- help="Generate the markdown file with the untranslated strings",
- )
- ensureMarkdownFilesMatchParser = commandParser.add_parser(
- "ensureMarkdownFilesMatch",
- )
- ensureMarkdownFilesMatchParser.add_argument(
- dest="path1",
- type=str,
- help="The first markdown file",
- )
- ensureMarkdownFilesMatchParser.add_argument(
- dest="path2",
- type=str,
- help="The second markdown file",
- )
- pretranslateLangsParser = commandParser.add_parser("pretranslateLangs")
- pretranslateLangsParser.add_argument(
- "-d",
- "--langs-dir",
- dest="langsDir",
- type=str,
- required=True,
- help="The directory containing the language directories",
- )
- pretranslateLangsParser.add_argument(
- "-b",
- "--md-base-name",
- dest="mdBaseName",
- type=str,
- required=True,
- help="The base name of the markdown files to pretranslate",
- )
- args = mainParser.parse_args()
- match args.command:
- case "generateXliff":
- generateXliff(mdPath=args.md, outputPath=args.output)
- case "updateXliff":
- updateXliff(
- xliffPath=args.xliff,
- mdPath=args.md,
- outputPath=args.output,
- )
- case "generateMarkdown":
- generateMarkdown(
- xliffPath=args.xliff,
- outputPath=args.output,
- translated=args.translated,
- )
- case "translateXliff":
- translateXliff(
- xliffPath=args.xliff,
- lang=args.lang,
- pretranslatedMdPath=args.pretranslatedMd,
- outputPath=args.output,
- )
- case "pretranslateLangs":
- pretranslateAllPossibleLanguages(
- langsDir=args.langsDir,
- mdBaseName=args.mdBaseName,
- )
- case "ensureMarkdownFilesMatch":
- ensureMarkdownFilesMatch(path1=args.path1, path2=args.path2)
- case _:
- raise ValueError(f"Unknown command: {args.command}")
diff --git a/.github/workflows/auto-update-translations.yaml b/.github/workflows/auto-update-translations.yaml
deleted file mode 100644
index 61d57c6..0000000
--- a/.github/workflows/auto-update-translations.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: Auto update translations
-
-on:
- push:
- branches:
- - main
- schedule:
- # * is a special character in YAML so you have to quote this string
- - cron: '0 0 * * 6'
-
-jobs:
- auto_update_translations:
- uses: abdel792/autoUpdateTranslations/.github/workflows/l10n-updates.yaml@8ac89c644395cf2aad6e03a4bb2be5c36526d12a
-
\ No newline at end of file
diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml
index 59f9ee1..ec1cc73 100644
--- a/.github/workflows/build_addon.yml
+++ b/.github/workflows/build_addon.yml
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout repo
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@@ -30,7 +30,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update -y
- sudo apt-get install -y gettext
+ sudo apt-get install -y gettext libgtk-3-dev
uv sync
- name: Code checks
@@ -60,7 +60,7 @@ jobs:
permissions:
contents: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: download all artifacts
uses: actions/download-artifact@v8
with:
diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml
index 5b79083..d742f52 100644
--- a/.github/workflows/crowdinL10n.yml
+++ b/.github/workflows/crowdinL10n.yml
@@ -15,6 +15,7 @@ env:
GH_TOKEN: ${{ github.token }}
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID || 780748 }}
L10N_UTIL_CONFIG: ${{ vars.L10N_UTIL_CONFIG || 'addon' }}
+ MIN_PERCENTAGE_TRANSLATED: ${{ vars.MIN_PERCENTAGE_TRANSLATED || 50 }}
jobs:
crowdinSync:
@@ -31,7 +32,7 @@ jobs:
Write-Host "Sleeping for $delaySeconds seconds..."
Start-Sleep -Seconds $delaySeconds
- name: Checkout add-on
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
submodules: true
- name: Set up Python
@@ -56,6 +57,7 @@ jobs:
run: |
gh release download --repo nvaccess/nvdaL10n --pattern "l10nUtil.exe"
- name: Download translations from Crowdin
+ if: ${{ env.crowdinAuthToken }}
shell: pwsh
env:
ADDON_ID: ${{ steps.getAddonInfo.outputs.addonId }}
diff --git a/.github/workflows/manual-release.yaml b/.github/workflows/manual-release.yaml
index 2a896f6..1482278 100644
--- a/.github/workflows/manual-release.yaml
+++ b/.github/workflows/manual-release.yaml
@@ -150,8 +150,8 @@ jobs:
with open("changelog.md", "w", encoding="utf-8") as f:
f.write(text)
-
- shell: python
+
+ shell: python
- name: Check if there are any changes
diff --git a/addon/appModules/mp3directcut.py b/addon/appModules/mp3directcut.py
index c15d99b..7eedc3d 100644
--- a/addon/appModules/mp3directcut.py
+++ b/addon/appModules/mp3directcut.py
@@ -12,6 +12,7 @@
import windowUtils
from typing import Callable
import controlTypes
+
if hasattr(controlTypes, "ROLE_PANE"):
from controlTypes import ROLE_PANE, ROLE_EDITABLETEXT
else:
@@ -29,112 +30,120 @@
setFocus,
mouse_event,
MOUSEEVENTF_LEFTDOWN,
- MOUSEEVENTF_LEFTUP
+ MOUSEEVENTF_LEFTUP,
)
from ui import message
import sys
from NVDAObjects.IAccessible import IAccessible, getNVDAObjectFromEvent
import addonHandler
+
addonHandler.initTranslation()
_: Callable[[str], str]
# Constants
-PROGRAM_NAME = 'mp3DirectCut'
+PROGRAM_NAME = "mp3DirectCut"
ADDON_SUMMARY = addonHandler.getCodeAddon().manifest["summary"]
-hr, min, sec, hun, th = _('hours'), _('minutes'), _('seconds'), _('hundredths'), _('thousandths')
+hr, min, sec, hun, th = _("hours"), _("minutes"), _("seconds"), _("hundredths"), _("thousandths")
# For support of speak on demand feature.
speakOnDemand = {"speakOnDemand": True} if buildVersion.version_year > 2023 else {}
announce = (
# Translators: Message to inform that no selection has been realized.
- _('No selection'),
+ _("No selection"),
# Translators: Message to inform the user that the playback cursor is at the top of the file.
- _('Beginning of the file.'),
+ _("Beginning of the file."),
# Translators: Message to inform the user that the playback cursor is at the end of the file.
- _('End of the file.'),
+ _("End of the file."),
# Translators: Message to inform the user that not file is loaded.
- _('Not file is loaded. Please check that you are in a file, open one with Control O,\
- or R to start recording.'),
+ _(
+ "Not file is loaded. Please check that you are in a file, open one with Control O,\
+ or R to start recording.",
+ ),
# Translators: Message to inform the user that the current command is not available in a recording mode.
- _('This command is not available in a recording mode, it is available only in a reading mode !'),
+ _("This command is not available in a recording mode, it is available only in a reading mode !"),
# Translators: Message to indicate the position of the selection start marker.
- _('The marker of the beginning of selection B is at'),
+ _("The marker of the beginning of selection B is at"),
# Translators: Message to indicate the position of the selection end marker.
- _('The marker of the End of selection N is at'),
+ _("The marker of the End of selection N is at"),
# Translators: Message to indicate the level of the vu-meter.
- _('The level of the vu-meter is at'),
+ _("The level of the vu-meter is at"),
# Translators: Message to indicate the total duration of the current file.
- _('Total time: '),
+ _("Total time: "),
# Translators: Message to indicate the duration of the selection.
- _('Duration of selection'),
+ _("Duration of selection"),
# Translators: Message to indicate the actual part.
- _('Actual part'),
+ _("Actual part"),
# Translators: Message to inform that no selection was found.
- _('Selection not found.'),
+ _("Selection not found."),
# Translators: Message to indicate that the information of the current part is not available.
- _('Information specific to the actual part is not available.'),
+ _("Information specific to the actual part is not available."),
# Translators: Message to prompt the user to verify that no selection has been made.
- _('Please check that no selection has been made.'),
+ _("Please check that no selection has been made."),
# Translators: Message to prompt the user to verify that it is not in recording mode.
- _('Please chek that you are not in a recording mode.'),
+ _("Please chek that you are not in a recording mode."),
# Translators: Message to inform the user that the recording is ready.
- _('The recording is ready ! It remains only to press spacebar for begin the recording.\
- This same spacebar will stop the recording !'),
+ _(
+ "The recording is ready ! It remains only to press spacebar for begin the recording.\
+ This same spacebar will stop the recording !",
+ ),
# Translators: Message to inform the user that a recording is in progress.
- _('A recording is in progress, please press spacebar for stop it and start a new one.'),
+ _("A recording is in progress, please press spacebar for stop it and start a new one."),
# Translators: Message to inform the user that the recording is not ready.
- _('The recording is not ready !'),
+ _("The recording is not ready !"),
# Translators: Message to indicate that the vu-meter is not available.
- _('The vu-meter is not available. Please verify if a recording is in progress,\
- and that the checkbox enable the margin button is checked in the options of {0}.').format(PROGRAM_NAME),
+ _(
+ "The vu-meter is not available. Please verify if a recording is in progress,\
+ and that the checkbox enable the margin button is checked in the options of {0}.",
+ ).format(PROGRAM_NAME),
# Translators: Message to confirm that the level of the vu-meter has been reset.
- _('The level of the vu-meter has been reset !'),
+ _("The level of the vu-meter has been reset !"),
# Translators: Message to confirm the placement of the selection start marker.
_("Start selection marker set."),
# Translators: Message to confirm the placement of the selection end marker.
_("End selection marker set."),
# Translators: Message to confirm that the selection has been canceled.
- _('Selection canceled.')
+ _("Selection canceled."),
)
def timeSplitter(time): # noqa: C901
- hours = minutes = seconds = hundredths = thousandths = ''
- if ':' in time:
- hrs = time.split(':')
- if hrs[0] != '00' and hrs[0] != '0':
- hours = '{0} {1}, '.format(hrs[0], hr)
- if hrs[1].split("'")[0] != '00':
+ hours = minutes = seconds = hundredths = thousandths = ""
+ if ":" in time:
+ hrs = time.split(":")
+ if hrs[0] != "00" and hrs[0] != "0":
+ hours = "{0} {1}, ".format(hrs[0], hr)
+ if hrs[1].split("'")[0] != "00":
minutes = hrs[1].split("'")[0]
else:
mnts = time.split("'")
- if mnts[0] != '00' and mnts[0] != '0':
+ if mnts[0] != "00" and mnts[0] != "0":
minutes = mnts[0]
if minutes:
if len(minutes) > 1:
- if minutes[0] == '0':
+ if minutes[0] == "0":
minutes = minutes[1]
- minutes = '{0} {1}, '.format(minutes, min)
+ minutes = "{0} {1}, ".format(minutes, min)
try:
- scnds = time.split("'")[1].split('.')[0]
+ scnds = time.split("'")[1].split(".")[0]
except IndexError:
- scnds = ''
- if scnds != '00' and scnds != '0':
+ scnds = ""
+ if scnds != "00" and scnds != "0":
seconds = scnds
if seconds:
if len(seconds) > 1:
- if seconds[0] == '0':
+ if seconds[0] == "0":
seconds = seconds[1]
- seconds = '{0} {1}, '.format(seconds, sec)
- hd = time.split('.')[1].split()[0]
- if hd != '00' and hd != '000':
+ seconds = "{0} {1}, ".format(seconds, sec)
+ hd = time.split(".")[1].split()[0]
+ if hd != "00" and hd != "000":
if len(hd) == 3:
- thousandths = '{0} {1}.'.format(hd, th)
+ thousandths = "{0} {1}.".format(hd, th)
else:
- hundredths = '{0} {1}.'.format(hd, hun)
- timeSplitter = hours + minutes + seconds + hundredths if not thousandths\
- else hours + minutes + seconds + thousandths
+ hundredths = "{0} {1}.".format(hd, hun)
+ timeSplitter = (
+ hours + minutes + seconds + hundredths if not thousandths else hours + minutes + seconds + thousandths
+ )
return timeSplitter
@@ -150,19 +159,21 @@ def isRecordingReady():
def sayMessage(msg, space=None, marker=None):
import config
+
if space:
- if config.conf['mp3DCReport']['space']:
+ if config.conf["mp3DCReport"]["space"]:
message(msg)
elif marker:
- if config.conf['mp3DCReport']['marker']:
+ if config.conf["mp3DCReport"]["marker"]:
message(msg)
else:
- if config.conf['mp3DCReport']['other']:
+ if config.conf["mp3DCReport"]["other"]:
message(msg)
def isReading():
import time
+
firstValue = actualDuration()
time.sleep(0.2)
secondValue = actualDuration()
@@ -191,7 +202,7 @@ def getTextFromWindow(hwnd):
def isStarting():
focus = api.getFocusObject()
- if focus.role == ROLE_PANE and focus.name == 'mp3DirectCut':
+ if focus.role == ROLE_PANE and focus.name == "mp3DirectCut":
hwnd = readingWindow()
starting = getTextFromWindow(hwnd)
if not starting:
@@ -220,7 +231,7 @@ def checkPart():
text = getTextFromWindow(hwnd)
if text and not text.isspace():
text = text.split()
- if text[1].startswith('('):
+ if text[1].startswith("("):
return True
return False
@@ -237,18 +248,18 @@ def checkSelection():
def part(flag=None):
hwnd = recAndSelWindow()
text = getTextFromWindow(hwnd)
- text = text.split('(')
+ text = text.split("(")
text = text[1]
- text = text.split(')')
+ text = text.split(")")
text = text[0]
- text = text.replace('/', ' {0} '.format(_('of')))
- return '{0} {1}'.format(announce[10], text) if not flag else '{0} {1}'.format(_('Part'), text)
+ text = text.replace("/", " {0} ".format(_("of")))
+ return "{0} {1}".format(announce[10], text) if not flag else "{0} {1}".format(_("Part"), text)
def selectionDuration():
hwnd = recAndSelWindow()
text = getTextFromWindow(hwnd)
- selectionDuration = text.split('(')
+ selectionDuration = text.split("(")
selectionDuration = selectionDuration[1]
selectionDuration = selectionDuration[:-1]
selectionDuration = timeSplitter(selectionDuration) if timeSplitter(selectionDuration) else announce[0]
@@ -258,7 +269,7 @@ def selectionDuration():
def beginSelection():
hwnd = recAndSelWindow()
text = getTextFromWindow(hwnd)
- beginSelection = text.split(' - ')
+ beginSelection = text.split(" - ")
beginSelection = beginSelection[0]
beginSelection = beginSelection.split()
if len(beginSelection) > 2:
@@ -272,9 +283,9 @@ def beginSelection():
def endSelection():
hwnd = recAndSelWindow()
text = getTextFromWindow(hwnd)
- endSelection = text.split(' - ')
+ endSelection = text.split(" - ")
endSelection = endSelection[1]
- endSelection = endSelection.split(' ')
+ endSelection = endSelection.split(" ")
endSelection = endSelection[0]
endSelection = timeSplitter(endSelection) if timeSplitter(endSelection) else announce[1]
return endSelection
@@ -283,8 +294,8 @@ def endSelection():
def actualDuration():
hwnd = readingWindow()
actual = getTextFromWindow(hwnd)
- if actual and not actual.isspace() and ' ' in actual:
- actual = actual.split(': ')
+ if actual and not actual.isspace() and " " in actual:
+ actual = actual.split(": ")
actual = actual[2].split()
actual = actual[0]
actual = timeSplitter(actual)
@@ -294,8 +305,8 @@ def actualDuration():
def actualDurationPercentage():
hwnd = readingWindow()
actual = getTextFromWindow(hwnd)
- if '(' in actual:
- actual = actual.split('(')
+ if "(" in actual:
+ actual = actual.split("(")
actual = actual[1]
actual = actual[:-1]
return actual
@@ -305,8 +316,8 @@ def totalTime():
if checkPart() or checkSelection():
hwnd = readingWindow()
time = getTextFromWindow(hwnd)
- time = time.split(': ')
- time = time[1].split(' ')[0]
+ time = time.split(": ")
+ time = time[1].split(" ")[0]
total = timeSplitter(time)
return total
@@ -314,27 +325,27 @@ def totalTime():
def timeRemaining():
hwnd = readingWindow()
actual = getTextFromWindow(hwnd)
- if actual != '' and not actual.isspace() and ' ' in actual:
- actual = actual.split(': ')
+ if actual != "" and not actual.isspace() and " " in actual:
+ actual = actual.split(": ")
actual = actual[2].split()
actual = actual[0]
total = getTextFromWindow(hwnd)
- total = total.split(': ')
+ total = total.split(": ")
total = total[1]
- total = total.split(' ')[0]
+ total = total.split(" ")[0]
if total == actual:
# Translators: Message to inform the user that there is no remaining time.
- return _('No time remaining !')
- hORm = len(total.split('.')[1])
+ return _("No time remaining !")
+ hORm = len(total.split(".")[1])
fmt = "%H:%M'%S.%f"
- if ':' not in actual:
- actual = '0:{0}'.format(actual)
- if ':' not in total:
- total = '0:{0}'.format(total)
+ if ":" not in actual:
+ actual = "0:{0}".format(actual)
+ if ":" not in total:
+ total = "0:{0}".format(total)
result = datetime.strptime(total, fmt) - datetime.strptime(actual, fmt)
- result = str(result).decode('utf-8') if sys.version_info.major == 2 else str(result)
- result = result.replace(':', "'")
- result = result.replace("'", ':', 1)
+ result = str(result).decode("utf-8") if sys.version_info.major == 2 else str(result)
+ result = result.replace(":", "'")
+ result = result.replace("'", ":", 1)
result = result[:-4] if hORm == 2 else result[:-3]
try:
return timeSplitter(result)
@@ -342,8 +353,7 @@ def timeRemaining():
return totalTime()
-class SoundManager (IAccessible):
-
+class SoundManager(IAccessible):
scriptCategory = ADDON_SUMMARY
keys = (
"kb:1",
@@ -366,7 +376,7 @@ def script_readFromSelection(self, gesture):
kig.fromName("downArrow").send()
@script(
- gesture='kb:r',
+ gesture="kb:r",
**speakOnDemand,
)
def script_checkRecording(self, gesture):
@@ -379,7 +389,7 @@ def script_checkRecording(self, gesture):
sayMessage(announce[17])
@script(
- gesture='kb:control+r',
+ gesture="kb:control+r",
**speakOnDemand,
)
def script_cancelSelection(self, gesture):
@@ -392,7 +402,7 @@ def script_cancelSelection(self, gesture):
message(text)
@script(
- gesture='kb:space',
+ gesture="kb:space",
**speakOnDemand,
)
def script_space(self, gesture):
@@ -414,13 +424,16 @@ def script_space(self, gesture):
if not actual:
sayMessage(announce[1], space=True)
return
- actual = actual + ' ' + actualDurationPercentage() if not actual == totalTime()\
- else announce[2] + ' ' + actual + ' ' + actualDurationPercentage()
+ actual = (
+ actual + " " + actualDurationPercentage()
+ if not actual == totalTime()
+ else announce[2] + " " + actual + " " + actualDurationPercentage()
+ )
sayMessage(actual, space=True)
self.appModule.runValueChange = True
@script(
- gesture='kb:control+rightArrow',
+ gesture="kb:control+rightArrow",
**speakOnDemand,
)
def script_nextSplittingPoint(self, gesture):
@@ -431,11 +444,17 @@ def script_nextSplittingPoint(self, gesture):
if checkPart() or checkSelection():
actual = actualDuration()
if actual == totalTime():
- actual = announce[2] + ' ' + actualDuration() + ' ' + actualDurationPercentage() if checkSelection()\
- else announce[2] + ' ' + actualDuration() + ' ' + part(flag=True)
+ actual = (
+ announce[2] + " " + actualDuration() + " " + actualDurationPercentage()
+ if checkSelection()
+ else announce[2] + " " + actualDuration() + " " + part(flag=True)
+ )
else:
- actual = actual + ' ' + actualDurationPercentage() if checkSelection()\
- else actual + ' ' + part(flag=True)
+ actual = (
+ actual + " " + actualDurationPercentage()
+ if checkSelection()
+ else actual + " " + part(flag=True)
+ )
if not isReading():
self.appModule.runValueChange = False
api.processPendingEvents()
@@ -443,7 +462,7 @@ def script_nextSplittingPoint(self, gesture):
self.appModule.runValueChange = True
@script(
- gesture='kb:control+leftArrow',
+ gesture="kb:control+leftArrow",
**speakOnDemand,
)
def script_previousSplittingPoint(self, gesture):
@@ -454,11 +473,17 @@ def script_previousSplittingPoint(self, gesture):
if checkSelection() or checkPart():
actual = actualDuration()
if not actual:
- actual = announce[1] + ' ' + actualDurationPercentage() if checkSelection()\
- else announce[1] + ' ' + part(flag=True)
+ actual = (
+ announce[1] + " " + actualDurationPercentage()
+ if checkSelection()
+ else announce[1] + " " + part(flag=True)
+ )
else:
- actual = actual + ' ' + actualDurationPercentage() if checkSelection()\
- else actual + ' ' + part(flag=True)
+ actual = (
+ actual + " " + actualDurationPercentage()
+ if checkSelection()
+ else actual + " " + part(flag=True)
+ )
if not isReading():
self.appModule.runValueChange = False
api.processPendingEvents()
@@ -466,7 +491,7 @@ def script_previousSplittingPoint(self, gesture):
self.appModule.runValueChange = True
@script(
- gesture='kb:upArrow',
+ gesture="kb:upArrow",
**speakOnDemand,
)
def script_up(self, gesture):
@@ -485,22 +510,28 @@ def script_up(self, gesture):
if not actual:
actual = announce[1]
if actual == totalTime():
- actual = '{0} {1}'.format(actual, announce[2])
- sayMessage(announce[5] + ' : ' + actual + ' ' + actualDurationPercentage())
+ actual = "{0} {1}".format(actual, announce[2])
+ sayMessage(announce[5] + " : " + actual + " " + actualDurationPercentage())
else:
actual = actualDuration()
if not actual:
- sayMessage('{0}, {1}'.format(announce[0], announce[1]))
+ sayMessage("{0}, {1}".format(announce[0], announce[1]))
return
if actual == totalTime():
- actual = '{0} {1}'.format(actual, announce[2])
+ actual = "{0} {1}".format(actual, announce[2])
# Translators: Message to indicate the elapsed time.
- sayMessage('{0}, {1} {2} {3}'.format(
- announce[0], _('Elapsed time: '), actual, actualDurationPercentage()))
+ sayMessage(
+ "{0}, {1} {2} {3}".format(
+ announce[0],
+ _("Elapsed time: "),
+ actual,
+ actualDurationPercentage(),
+ ),
+ )
self.appModule.runValueChange = True
@script(
- gesture='kb:downArrow',
+ gesture="kb:downArrow",
**speakOnDemand,
)
def script_down(self, gesture):
@@ -519,25 +550,33 @@ def script_down(self, gesture):
if not actual:
actual = announce[1]
if actual == totalTime():
- actual = '{0} {1}'.format(actual, announce[2])
- sayMessage(announce[6] + ' : ' + actual + ' ' + actualDurationPercentage())
+ actual = "{0} {1}".format(actual, announce[2])
+ sayMessage(announce[6] + " : " + actual + " " + actualDurationPercentage())
else:
actual = actualDuration()
if not actual:
- sayMessage('{0}, {1}'.format(announce[0], announce[1]))
+ sayMessage("{0}, {1}".format(announce[0], announce[1]))
return
if actual == totalTime():
- actual = '{0} {1}'.format(actual, announce[2])
+ actual = "{0} {1}".format(actual, announce[2])
# Translators: Message to indicate the elapsed time.
- sayMessage('{0}, {1} {2} {3}'.format(
- announce[0], _('Elapsed time: '), actual, actualDurationPercentage()))
+ sayMessage(
+ "{0}, {1} {2} {3}".format(
+ announce[0],
+ _("Elapsed time: "),
+ actual,
+ actualDurationPercentage(),
+ ),
+ )
self.appModule.runValueChange = True
@script(
- gesture='kb:control+shift+d',
+ gesture="kb:control+shift+d",
# Translators, Message presented in input help mode.
- description=_('Gives the duration from the beginning of the file to the current position of the playback cursor.\
- If pressed twice, gives the total duration.'),
+ description=_(
+ "Gives the duration from the beginning of the file to the current position of the playback cursor.\
+ If pressed twice, gives the total duration.",
+ ),
**speakOnDemand,
)
def script_elapsedTime(self, gesture):
@@ -551,22 +590,28 @@ def script_elapsedTime(self, gesture):
if not actualDuration():
text = announce[1]
elif actualDuration() == totalTime():
- text = '{0} {1} {2} {3}'.format(
- _('Elapsed time: '), actualDuration(), announce[2], actualDurationPercentage())
+ text = "{0} {1} {2} {3}".format(
+ _("Elapsed time: "),
+ actualDuration(),
+ announce[2],
+ actualDurationPercentage(),
+ )
else:
# Translators: Message to indicate the elapsed time.
- text = '{0} {1} {2}'.format(_('Elapsed time: '), actualDuration(), actualDurationPercentage())
+ text = "{0} {1} {2}".format(_("Elapsed time: "), actualDuration(), actualDurationPercentage())
repeat = getLastScriptRepeatCount()
if repeat == 0:
message(text)
elif repeat == 1:
- message('{0} {1}'.format(announce[8], totalTime()))
+ message("{0} {1}".format(announce[8], totalTime()))
@script(
- gesture='kb:control+shift+r',
+ gesture="kb:control+shift+r",
# Translators: Message presented in input help mode.
- description=_('Gives the time remaining from the current position of the playback cursor '
- 'to the end of the file.'),
+ description=_(
+ "Gives the time remaining from the current position of the playback cursor "
+ "to the end of the file.",
+ ),
**speakOnDemand,
)
def script_timeRemaining(self, gesture):
@@ -578,13 +623,15 @@ def script_timeRemaining(self, gesture):
return
if checkSelection() or checkPart():
# Translators: Message to indicate the remaining time.
- message('{0} {1}'.format(_('Remaining time: '), timeRemaining()))
+ message("{0} {1}".format(_("Remaining time: "), timeRemaining()))
@script(
- gesture='kb:control+shift+space',
+ gesture="kb:control+shift+space",
# Translators: Message presented in input help mode.
- description=_('Used to determine the current level of the vu-meter, '
- 'during recording, double pressure reset it.'),
+ description=_(
+ "Used to determine the current level of the vu-meter, "
+ "during recording, double pressure reset it.",
+ ),
**speakOnDemand,
)
def script_vuMeter(self, gesture):
@@ -597,7 +644,7 @@ def script_vuMeter(self, gesture):
level = getTextFromWindow(hwnd)
if level:
if repeat == 0:
- sayMessage(announce[7] + ' : ' + level)
+ sayMessage(announce[7] + " : " + level)
elif repeat == 1:
setFocus(hwnd)
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, None, None)
@@ -607,7 +654,7 @@ def script_vuMeter(self, gesture):
sayMessage(announce[18])
@script(
- gesture='kb:b',
+ gesture="kb:b",
**speakOnDemand,
)
def script_bPosition(self, gesture):
@@ -619,7 +666,7 @@ def script_bPosition(self, gesture):
sayMessage(announce[20], marker=True)
@script(
- gesture='kb:n',
+ gesture="kb:n",
**speakOnDemand,
)
def script_nPosition(self, gesture):
@@ -631,10 +678,12 @@ def script_nPosition(self, gesture):
sayMessage(announce[21], marker=True)
@script(
- gesture='kb:control+shift+b',
+ gesture="kb:control+shift+b",
# Translators: Message presented in input help mode.
- description=_('Used to indicate the position of the marker of the beginning of selection B.\
- Double pressure lets give you the duration of the selection.'),
+ description=_(
+ "Used to indicate the position of the marker of the beginning of selection B.\
+ Double pressure lets give you the duration of the selection.",
+ ),
**speakOnDemand,
)
def script_beginningOfSelection(self, gesture):
@@ -645,17 +694,19 @@ def script_beginningOfSelection(self, gesture):
if checkSelection():
bSelection = beginSelection()
if repeat == 0:
- sayMessage(announce[5] + ' : ' + bSelection)
+ sayMessage(announce[5] + " : " + bSelection)
elif repeat == 1:
- sayMessage(announce[9] + ' : ' + selectionDuration())
+ sayMessage(announce[9] + " : " + selectionDuration())
else:
sayMessage(announce[11])
@script(
- gesture='kb:control+shift+e',
+ gesture="kb:control+shift+e",
# Translators: Message presented in input help mode.
- description=_('Used to indicate the position of the marker of the end of selection N.\
- Double pressure gives recapitulatif positions B and N, and the duration of the selection.'),
+ description=_(
+ "Used to indicate the position of the marker of the end of selection N.\
+ Double pressure gives recapitulatif positions B and N, and the duration of the selection.",
+ ),
**speakOnDemand,
)
def script_endOfSelection(self, gesture):
@@ -667,18 +718,20 @@ def script_endOfSelection(self, gesture):
bSelection = beginSelection()
eSelection = endSelection()
if repeat == 0:
- sayMessage(announce[6] + ' : ' + eSelection)
+ sayMessage(announce[6] + " : " + eSelection)
elif repeat == 1:
- sayMessage(announce[5] + ' : ' + bSelection)
- sayMessage(announce[6] + ' : ' + eSelection)
- sayMessage(announce[9] + ' : ' + selectionDuration())
+ sayMessage(announce[5] + " : " + bSelection)
+ sayMessage(announce[6] + " : " + eSelection)
+ sayMessage(announce[9] + " : " + selectionDuration())
else:
sayMessage(announce[11])
@script(
- gesture='kb:control+shift+p',
+ gesture="kb:control+shift+p",
# Translators: Message presented in input help mode.
- description=_('Give the reference of the actual part and the total number of parts in the current file.'),
+ description=_(
+ "Give the reference of the actual part and the total number of parts in the current file.",
+ ),
**speakOnDemand,
)
def script_actualPart(self, gesture):
@@ -688,30 +741,29 @@ def script_actualPart(self, gesture):
if checkPart():
message(part())
elif isRecording():
- message(announce[12] + ' ' + announce[14])
+ message(announce[12] + " " + announce[14])
elif checkSelection():
- message(announce[12] + ' ' + announce[13])
+ message(announce[12] + " " + announce[13])
else:
message(announce[12])
-class AppModule (appModuleHandler.AppModule):
-
+class AppModule(appModuleHandler.AppModule):
scriptCategory = ADDON_SUMMARY
runValueChange = True
def event_valueChange(self, obj, nextHandler):
if not self.runValueChange:
return
- if obj.role == ROLE_EDITABLETEXT and obj.value and all(x in obj.value for x in [' ', ':']):
+ if obj.role == ROLE_EDITABLETEXT and obj.value and all(x in obj.value for x in [" ", ":"]):
if checkSelection() or checkPart():
actual = actualDuration()
if actual == totalTime():
- actual = announce[2] + ' ' + actual
+ actual = announce[2] + " " + actual
elif not actual:
actual = announce[1]
else:
- actual = actual + ' ' + actualDurationPercentage()
+ actual = actual + " " + actualDurationPercentage()
if not isReading():
sayMessage(actual)
return
@@ -720,19 +772,19 @@ def event_valueChange(self, obj, nextHandler):
def event_NVDAObject_init(self, obj):
if obj and obj.firstChild and obj.firstChild.windowControlID == 641:
obj.name = obj.firstChild.name
- if self.productVersion < '2.2.1':
+ if self.productVersion < "2.2.1":
if "#" in obj.displayText:
match = re.search(r"(^.+?#[0-9]+)", obj.displayText)
if match:
obj.name = match.group(1)
def chooseNVDAObjectOverlayClasses(self, obj, clsList):
- if obj.role == ROLE_PANE and obj.name and any(x in obj.name for x in ['mp3DirectCut', '.mp3']):
+ if obj.role == ROLE_PANE and obj.name and any(x in obj.name for x in ["mp3DirectCut", ".mp3"]):
clsList.insert(0, SoundManager)
@script(
gesture="kb:nvda+h",
- description=_('Lets open the help of the current add-on.'),
+ description=_("Lets open the help of the current add-on."),
**speakOnDemand,
)
def script_openHelp(self, gesture):
diff --git a/addon/globalPlugins/mp3DirectCut/__init__.py b/addon/globalPlugins/mp3DirectCut/__init__.py
index 40fdc99..2941a46 100644
--- a/addon/globalPlugins/mp3DirectCut/__init__.py
+++ b/addon/globalPlugins/mp3DirectCut/__init__.py
@@ -16,6 +16,7 @@
import gui
import wx
import config
+
addonHandler.initTranslation()
_: Callable[[str], str]
# Constants
@@ -25,15 +26,14 @@
speakOnDemand = {"speakOnDemand": True} if buildVersion.version_year > 2023 else {}
confSpec = {
- 'space': 'boolean(default = True)',
- 'marker': 'boolean(default = True)',
- 'other': 'boolean(default = True)'
+ "space": "boolean(default = True)",
+ "marker": "boolean(default = True)",
+ "other": "boolean(default = True)",
}
-config.conf.spec['mp3DCReport'] = confSpec
-
+config.conf.spec["mp3DCReport"] = confSpec
-class GlobalPlugin (globalPluginHandler.GlobalPlugin):
+class GlobalPlugin(globalPluginHandler.GlobalPlugin):
scriptCategory = ADDON_SUMMARY
def __init__(self, *args, **kwargs):
@@ -43,6 +43,7 @@ def __init__(self, *args, **kwargs):
# This block ensures compatibility with NVDA versions prior to 2018.2 which includes the settings panel.
if hasattr(gui, "NVDASettingsDialog"):
from gui import NVDASettingsDialog
+
NVDASettingsDialog.categoryClasses.append(Mp3DirectCutPanel)
else:
self.createSubMenu()
@@ -52,7 +53,8 @@ def createSubMenu(self):
self.mp3DirectCut = self.preferencesMenu.Append(
wx.ID_ANY,
# Translators: name of the option in the menu.
- _("{0} addon configuration").format("mp3DirectCut"), ""
+ _("{0} addon configuration").format("mp3DirectCut"),
+ "",
)
gui.mainFrame.sysTrayIcon.Bind(wx.EVT_MENU, self.onMp3DirectCutDialog, self.mp3DirectCut)
@@ -77,14 +79,19 @@ def onMp3DirectCutDialog(self, evt):
def script_activateMP3DirectCutConfigurationDialog(self, gesture):
if not hasattr(
gui.settingsDialogs,
- "SettingsPanel"
+ "SettingsPanel",
) and not hasattr(
gui,
- "SettingsPanel"
+ "SettingsPanel",
):
wx.CallAfter(self.onMP3DirectCutDialog, None)
return
wx.CallAfter(
- (gui.mainFrame.popupSettingsDialog if hasattr(gui.mainFrame, "popupSettingsDialog")
- else gui.mainFrame._popupSettingsDialog),
- gui.settingsDialogs.NVDASettingsDialog, Mp3DirectCutPanel)
+ (
+ gui.mainFrame.popupSettingsDialog
+ if hasattr(gui.mainFrame, "popupSettingsDialog")
+ else gui.mainFrame._popupSettingsDialog
+ ),
+ gui.settingsDialogs.NVDASettingsDialog,
+ Mp3DirectCutPanel,
+ )
diff --git a/addon/globalPlugins/mp3DirectCut/mp3DirectCutDialog.py b/addon/globalPlugins/mp3DirectCut/mp3DirectCutDialog.py
index ac60fbc..98fb80a 100644
--- a/addon/globalPlugins/mp3DirectCut/mp3DirectCutDialog.py
+++ b/addon/globalPlugins/mp3DirectCut/mp3DirectCutDialog.py
@@ -11,6 +11,7 @@
import wx
from gui import guiHelper
from typing import Callable
+
# This block ensures compatibility with NVDA versions prior to 2018.2 which includes the settings panel.
try:
from gui.settingsDialogs import SettingsPanel
@@ -22,37 +23,36 @@
_: Callable[[str], str]
-class Mp3DirectCutPanel (SettingsPanel):
-
+class Mp3DirectCutPanel(SettingsPanel):
title = "mp3DirectCut"
def makeSettings(self, settingsSizer):
settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer)
# Translators: The label of the checkbox to enable or disable the spacebar announcements.
self.reportSpaceCheckBox = wx.CheckBox(parent=self, label=_("Enable announcements of the space key"))
- self.reportSpaceCheckBox.SetValue(config.conf['mp3DCReport']['space'])
+ self.reportSpaceCheckBox.SetValue(config.conf["mp3DCReport"]["space"])
settingsSizerHelper.addItem(self.reportSpaceCheckBox)
# Translators: The label of the checkbox to enable or disable the announcements of the selection markers.
self.reportMarkerCheckBox = wx.CheckBox(
parent=self,
- label=_("Announce the placement of the selection markers"))
- self.reportMarkerCheckBox.SetValue(config.conf['mp3DCReport']['marker'])
+ label=_("Announce the placement of the selection markers"),
+ )
+ self.reportMarkerCheckBox.SetValue(config.conf["mp3DCReport"]["marker"])
settingsSizerHelper.addItem(self.reportMarkerCheckBox)
# Translators: The label of the checkbox to enable or disable the other announcements.
self.reportOtherCheckBox = wx.CheckBox(parent=self, label=_("Enable the other announces"))
- self.reportOtherCheckBox.SetValue(config.conf['mp3DCReport']['other'])
+ self.reportOtherCheckBox.SetValue(config.conf["mp3DCReport"]["other"])
settingsSizerHelper.addItem(self.reportOtherCheckBox)
def onSave(self):
- config.conf['mp3DCReport']['space'] = self.reportSpaceCheckBox.GetValue()
- config.conf['mp3DCReport']['marker'] = self.reportMarkerCheckBox.GetValue()
- config.conf['mp3DCReport']['other'] = self.reportOtherCheckBox.GetValue()
-
+ config.conf["mp3DCReport"]["space"] = self.reportSpaceCheckBox.GetValue()
+ config.conf["mp3DCReport"]["marker"] = self.reportMarkerCheckBox.GetValue()
+ config.conf["mp3DCReport"]["other"] = self.reportOtherCheckBox.GetValue()
-class Mp3DirectCutDialog (SettingsDialog):
+class Mp3DirectCutDialog(SettingsDialog):
# Translators: The title of the add-on configuration dialog box.
title = _("Configuration of the addon {0}").format("mp3DirectCut")
@@ -60,26 +60,27 @@ def makeSettings(self, settingsSizer):
settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer)
# Translators: The label of the checkbox to enable or disable the spacebar announcements.
self.reportSpaceCheckBox = wx.CheckBox(parent=self, label=_("Enable announcements of the space key"))
- self.reportSpaceCheckBox.SetValue(config.conf['mp3DCReport']['space'])
+ self.reportSpaceCheckBox.SetValue(config.conf["mp3DCReport"]["space"])
settingsSizerHelper.addItem(self.reportSpaceCheckBox)
# Translators: The label of the checkbox to enable or disable the announcements of the selection markers.
self.reportMarkerCheckBox = wx.CheckBox(
parent=self,
- label=_("Announce the placement of the selection markers"))
- self.reportMarkerCheckBox.SetValue(config.conf['mp3DCReport']['marker'])
+ label=_("Announce the placement of the selection markers"),
+ )
+ self.reportMarkerCheckBox.SetValue(config.conf["mp3DCReport"]["marker"])
settingsSizerHelper.addItem(self.reportMarkerCheckBox)
# Translators: The label of the checkbox to enable or disable the other announcements.
self.reportOtherCheckBox = wx.CheckBox(parent=self, label=_("Enable the other announces"))
- self.reportOtherCheckBox.SetValue(config.conf['mp3DCReport']['other'])
+ self.reportOtherCheckBox.SetValue(config.conf["mp3DCReport"]["other"])
settingsSizerHelper.addItem(self.reportOtherCheckBox)
def postInit(self):
self.reportSpaceCheckBox.SetFocus()
def onOk(self, evt):
- config.conf['mp3DCReport']['space'] = self.reportSpaceCheckBox.GetValue()
- config.conf['mp3DCReport']['marker'] = self.reportMarkerCheckBox.GetValue()
- config.conf['mp3DCReport']['other'] = self.reportOtherCheckBox.GetValue()
+ config.conf["mp3DCReport"]["space"] = self.reportSpaceCheckBox.GetValue()
+ config.conf["mp3DCReport"]["marker"] = self.reportMarkerCheckBox.GetValue()
+ config.conf["mp3DCReport"]["other"] = self.reportOtherCheckBox.GetValue()
super(Mp3DirectCutDialog, self).onOk(evt)
diff --git a/addon/installTasks.py b/addon/installTasks.py
index 50f022d..7963bc1 100644
--- a/addon/installTasks.py
+++ b/addon/installTasks.py
@@ -10,6 +10,7 @@
import gui
from typing import Callable
import wx
+
addonHandler.initTranslation()
_: Callable[[str], str]
@@ -18,12 +19,17 @@ def onInstall():
addon = [x for x in addonHandler.getAvailableAddons() if x.manifest["name"] == "mp3directcut"]
if len(addon) > 0:
addon = addon[0]
- if gui.messageBox(
- # Translators: A message informing the user that he has installed an incompatible version.
- _("You have installed the mp3DirectCut-1.1 add-on which is incompatible with this one.\
- Do you want to uninstall this old version?"),
- # Translators: The title of the dialogbox.
- _("Uninstall incompatible version"),
- wx.YES | wx.NO | wx.ICON_WARNING
- ) == wx.YES:
+ if (
+ gui.messageBox(
+ # Translators: A message informing the user that he has installed an incompatible version.
+ _(
+ "You have installed the mp3DirectCut-1.1 add-on which is incompatible with this one.\
+ Do you want to uninstall this old version?",
+ ),
+ # Translators: The title of the dialogbox.
+ _("Uninstall incompatible version"),
+ wx.YES | wx.NO | wx.ICON_WARNING,
+ )
+ == wx.YES
+ ):
addon.requestRemove()
diff --git a/pyproject.toml b/pyproject.toml
index 5ab8998..9e9d82d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,8 @@ dependencies = [
"uv==0.11.6",
"ruff==0.14.5",
"pre-commit==4.2.0",
- "pyright[nodejs]==1.1.407",
+ "pyright[nodejs]==1.1.411",
+ "wxpython>=4.2.5",
]
[project.urls]
Repository = "https://github.com/abdel792/mp3DirectCut"
@@ -102,9 +103,6 @@ exclude = [
".venv",
"site_scons",
".github/scripts",
- # When excluding concrete paths relative to a directory,
- # not matching multiple folders by name e.g. `__pycache__`,
- # paths are relative to the configuration file.
]
# Tell pyright where to load python code from
@@ -124,92 +122,101 @@ strictDictionaryInference = true
strictSetInference = true
# Compliant rules
-reportAbstractUsage = true
-reportArgumentType = true
reportAssertAlwaysTrue = true
reportAssertTypeFailure = true
-reportAssignmentType = true
-reportAttributeAccessIssue = true
-reportCallInDefaultInitializer = true
-reportCallIssue = true
-reportConstantRedefinition = true
reportDuplicateImport = true
-reportFunctionMemberAccess = true
-reportGeneralTypeIssues = true
-reportImplicitOverride = true
-reportImplicitStringConcatenation = true
-reportImportCycles = true
-reportIncompatibleMethodOverride = true
-reportIncompatibleVariableOverride = true
reportIncompleteStub = true
-reportInconsistentConstructor = true
reportInconsistentOverload = true
-reportIndexIssue = true
+reportInconsistentConstructor = true
reportInvalidStringEscapeSequence = true
reportInvalidStubStatement = true
-reportInvalidTypeArguments = true
-reportInvalidTypeForm = true
reportInvalidTypeVarUse = true
reportMatchNotExhaustive = true
-reportMissingImports = true
reportMissingModuleSource = true
-reportMissingParameterType = true
-reportMissingSuperCall = true
-reportMissingTypeArgument = true
+reportMissingImports = true
reportNoOverloadImplementation = true
-reportOperatorIssue = true
-reportOptionalCall = true
reportOptionalContextManager = true
-reportOptionalIterable = true
-reportOptionalMemberAccess = true
-reportOptionalOperand = true
-reportOptionalSubscript = true
reportOverlappingOverload = true
-reportPossiblyUnboundVariable = true
reportPrivateImportUsage = true
-reportPrivateUsage = true
reportPropertyTypeMismatch = true
-reportRedeclaration = true
-reportReturnType = true
reportSelfClsParameterName = true
reportTypeCommentUsage = true
reportTypedDictNotRequiredAccess = true
-reportUnboundVariable = true
reportUndefinedVariable = true
+reportUnusedExpression = true
+reportUnboundVariable = true
reportUnhashable = true
-reportUninitializedInstanceVariable = true
-reportUnknownArgumentType = true
-reportUnknownLambdaType = true
-reportUnknownMemberType = true
-reportUnknownParameterType = true
-reportUnknownVariableType = true
reportUnnecessaryCast = true
-reportUnnecessaryComparison = true
reportUnnecessaryContains = true
-reportUnnecessaryIsInstance = true
reportUnnecessaryTypeIgnoreComment = true
-reportUnsupportedDunderAll = true
-reportUntypedBaseClass = true
reportUntypedClassDecorator = true
reportUntypedFunctionDecorator = true
-reportUntypedNamedTuple = true
-reportUnusedCallResult = true
reportUnusedClass = true
reportUnusedCoroutine = true
reportUnusedExcept = true
-reportUnusedExpression = true
-reportUnusedFunction = true
-reportUnusedImport = true
-reportUnusedVariable = true
-reportWildcardImportFromLibrary = true
-reportDeprecated = true
+# Should switch to true when possible
+reportDeprecated = false
# Can be enabled by generating type stubs for modules via pyright CLI
reportMissingTypeStubs = false
# Bad rules
-# These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved.
+# These are roughly sorted by compliance to make it easier for devs to focus on enabling them.
+# 1-50 errors
+reportUnsupportedDunderAll = false
+reportAbstractUsage = false
+reportUntypedBaseClass = false
+reportOptionalIterable = false
+reportCallInDefaultInitializer = false
+reportInvalidTypeArguments = false
+reportUntypedNamedTuple = false
+reportRedeclaration = false
+reportOptionalCall = false
+reportConstantRedefinition = false
+reportWildcardImportFromLibrary = false
+reportIncompatibleVariableOverride = false
+reportInvalidTypeForm = false
+
+# 50-100 errors
+reportGeneralTypeIssues = false
+reportOptionalOperand = false
+reportUnnecessaryComparison = false
+reportFunctionMemberAccess = false
+reportUnnecessaryIsInstance = false
+reportUnusedFunction = false
+reportImportCycles = false
+reportUnusedImport = false
+reportUnusedVariable = false
+
+# 100-1000 errors
+reportOperatorIssue = false
+reportAssignmentType = false
+reportReturnType = false
+reportPossiblyUnboundVariable = false
+reportMissingSuperCall = false
+reportUninitializedInstanceVariable = false
+reportUnknownLambdaType = false
+reportMissingTypeArgument = false
+reportImplicitStringConcatenation = false
+reportIncompatibleMethodOverride = false
+reportPrivateUsage = false
+
+# 1000+ errors
+reportUnusedCallResult = false
+reportOptionalSubscript = false
+reportCallIssue = false
+reportOptionalMemberAccess = false
+reportImplicitOverride = false
+reportIndexIssue = false
+reportAttributeAccessIssue = false
+reportArgumentType = false
+reportUnknownParameterType = false
+reportMissingParameterType = false
+reportUnknownVariableType = false
+reportUnknownArgumentType = false
+reportUnknownMemberType = false
+
[tool.mypy]
warn_unused_configs = true
ignore_missing_imports = true
diff --git a/readme.md b/readme.md
index fc867ed..8fa618c 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
-# mp3DirectCut
+# mp3DirectCut
* Author(s) : Abdel, Rémy, Abdellah zineddine, Jean-François COLAS.
diff --git a/uv.lock b/uv.lock
index 7e17b67..68fa380 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,47 +2,13 @@ version = 1
revision = 3
requires-python = "==3.13.*"
-[[package]]
-name = "addontemplate"
-source = { editable = "." }
-dependencies = [
- { name = "crowdin-api-client" },
- { name = "lxml" },
- { name = "markdown" },
- { name = "markdown-link-attr-modifier" },
- { name = "mdx-gh-links" },
- { name = "mdx-truly-sane-lists" },
- { name = "nh3" },
- { name = "pre-commit" },
- { name = "pyright", extra = ["nodejs"] },
- { name = "ruff" },
- { name = "scons" },
- { name = "uv" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "crowdin-api-client", specifier = "==1.24.1" },
- { name = "lxml", specifier = "==6.1.0" },
- { name = "markdown", specifier = "==3.10" },
- { name = "markdown-link-attr-modifier", specifier = "==0.2.1" },
- { name = "mdx-gh-links", specifier = "==0.4" },
- { name = "mdx-truly-sane-lists", specifier = "==1.3" },
- { name = "nh3", specifier = "==0.3.2" },
- { name = "pre-commit", specifier = "==4.2.0" },
- { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" },
- { name = "ruff", specifier = "==0.14.5" },
- { name = "scons", specifier = "==4.10.1" },
- { name = "uv", specifier = "==0.11.6" },
-]
-
[[package]]
name = "certifi"
-version = "2026.4.22"
+version = "2026.5.20"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
+ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
]
[[package]]
@@ -106,20 +72,20 @@ wheels = [
[[package]]
name = "distlib"
-version = "0.4.0"
+version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/8d/873e9252ea2c0e0c857884e0a2899ec43ade132345df1925ef24cbe64f18/distlib-0.4.2.tar.gz", hash = "sha256:baeb401c90f27acd15c4861ae0847d1e731c27ac3dbf4210643ba61fa1e813db", size = 614914, upload-time = "2026-06-08T16:24:15.439Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/60/aa891c893821d4d127292ed66c6940d1d715894bd5a0ce048056bc641773/distlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:ca4cb11e5d746b5ec13c199cbf19ae27a241f89702b54e153a74332955446067", size = 470510, upload-time = "2026-06-08T16:24:13.208Z" },
]
[[package]]
name = "filelock"
-version = "3.29.0"
+version = "3.29.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" },
]
[[package]]
@@ -133,11 +99,11 @@ wheels = [
[[package]]
name = "idna"
-version = "3.13"
+version = "3.18"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
@@ -211,6 +177,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/9e/dcd1027f7fd193aed152e01c6651a197c36b858f2cd1425ad04cb31a34fc/mdx_truly_sane_lists-1.3-py3-none-any.whl", hash = "sha256:b9546a4c40ff8f1ab692f77cee4b6bfe8ddf9cccf23f0a24e71f3716fe290a37", size = 6071, upload-time = "2022-07-19T13:42:43.375Z" },
]
+[[package]]
+name = "mp3directcut"
+source = { editable = "." }
+dependencies = [
+ { name = "crowdin-api-client" },
+ { name = "lxml" },
+ { name = "markdown" },
+ { name = "markdown-link-attr-modifier" },
+ { name = "mdx-gh-links" },
+ { name = "mdx-truly-sane-lists" },
+ { name = "nh3" },
+ { name = "pre-commit" },
+ { name = "pyright", extra = ["nodejs"] },
+ { name = "ruff" },
+ { name = "scons" },
+ { name = "uv" },
+ { name = "wxpython" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "crowdin-api-client", specifier = "==1.24.1" },
+ { name = "lxml", specifier = "==6.1.0" },
+ { name = "markdown", specifier = "==3.10" },
+ { name = "markdown-link-attr-modifier", specifier = "==0.2.1" },
+ { name = "mdx-gh-links", specifier = "==0.4" },
+ { name = "mdx-truly-sane-lists", specifier = "==1.3" },
+ { name = "nh3", specifier = "==0.3.2" },
+ { name = "pre-commit", specifier = "==4.2.0" },
+ { name = "pyright", extras = ["nodejs"], specifier = "==1.1.411" },
+ { name = "ruff", specifier = "==0.14.5" },
+ { name = "scons", specifier = "==4.10.1" },
+ { name = "uv", specifier = "==0.11.6" },
+ { name = "wxpython", specifier = ">=4.2.5" },
+]
+
[[package]]
name = "nh3"
version = "0.3.2"
@@ -245,27 +247,27 @@ wheels = [
[[package]]
name = "nodejs-wheel-binaries"
-version = "24.15.0"
+version = "24.16.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/70/a1e4f4d5986768ab90cc860b1cc3660fd2ded74ca175a900a5c29f839c7d/nodejs_wheel_binaries-24.15.0.tar.gz", hash = "sha256:b43f5c4f6e5768d8845b2ae4682eb703a19bf7aadc84187e2d903ed3a611c859", size = 8057, upload-time = "2026-04-19T15:48:16.899Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37", size = 8056, upload-time = "2026-05-30T16:52:09.807Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/85/66/54051d14853d6ab4fb85f8be9b042b530be653357fb9a19557498bc91ab7/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:a6232fa8b754220941f52388c8ead923f7c1c7fdf0ea0d98f657523bd9a81ef4", size = 55173485, upload-time = "2026-04-19T15:47:34.561Z" },
- { url = "https://files.pythonhosted.org/packages/ad/5f/66acada164da5ca10a0824db021aa7394ae18396c550cd9280e839a43126/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:001a6b62c69d9109c1738163cca00608dd2722e8663af59300054ea02610972d", size = 55348100, upload-time = "2026-04-19T15:47:40.521Z" },
- { url = "https://files.pythonhosted.org/packages/0d/2d/0cbd5ff40c9bb030ca1735d8f8793bd74f08a4cbd49100a1d19313ea57ab/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0fbc48765e60ed0ff30d43898dbf5cadbadf2e5f1e7f204afc2b01493b7ebce6", size = 59668206, upload-time = "2026-04-19T15:47:46.848Z" },
- { url = "https://files.pythonhosted.org/packages/da/d5/91ac63951ec75927a486b83b8cafe650e360fa70ac01dc94adfb32b93b97/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:20ee0536809795da8a4942fc1ab4cbdebbcaaf29383eab67ba8874268fb00008", size = 60206736, upload-time = "2026-04-19T15:47:52.668Z" },
- { url = "https://files.pythonhosted.org/packages/db/72/dc22776974d928869c0c30d23ee98ed7df254243c2df68f09f5963e8e8b8/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fade6c214285e72472ca40a631e98ff36559671cd5eefc8bf009471d67f04b4", size = 61720456, upload-time = "2026-04-19T15:47:58.325Z" },
- { url = "https://files.pythonhosted.org/packages/01/0a/34461b9050cb45ee371dccdefc622aef6351506ea2691b08fc761ca67150/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3984cb8d87766567aee67a49743227ab40ede6f47734ec990ff90e50b74e7740", size = 62326172, upload-time = "2026-04-19T15:48:04.094Z" },
- { url = "https://files.pythonhosted.org/packages/c9/17/09252bf35672dba926649d59dfe51443a0f6955ad13784e91131d5ec82a2/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:a437601956b532dcb3082046e6978e622733f90edc0932cbb9adb3bb97a16501", size = 41543461, upload-time = "2026-04-19T15:48:09.332Z" },
- { url = "https://files.pythonhosted.org/packages/0a/7e/b649777d148e1e0c2ce349156603cdb12f7ed99921b95d93717393650193/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:bdf4a431e08321a32efc604111c6f23941f87055d796a537e8c4110daecad23f", size = 39233248, upload-time = "2026-04-19T15:48:13.326Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c", size = 55666374, upload-time = "2026-05-30T16:51:39.588Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51", size = 55838487, upload-time = "2026-05-30T16:51:43.383Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342", size = 60179540, upload-time = "2026-05-30T16:51:47.036Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd", size = 60716262, upload-time = "2026-05-30T16:51:50.711Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502", size = 62230592, upload-time = "2026-05-30T16:51:55Z" },
+ { url = "https://files.pythonhosted.org/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03", size = 62841759, upload-time = "2026-05-30T16:51:59.407Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa", size = 42027734, upload-time = "2026-05-30T16:52:03.348Z" },
+ { url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" },
]
[[package]]
name = "platformdirs"
-version = "4.9.6"
+version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
+ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
]
[[package]]
@@ -286,15 +288,15 @@ wheels = [
[[package]]
name = "pyright"
-version = "1.1.407"
+version = "1.1.411"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
]
[package.optional-dependencies]
@@ -304,15 +306,15 @@ nodejs = [
[[package]]
name = "python-discovery"
-version = "1.2.2"
+version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" },
]
[[package]]
@@ -335,7 +337,7 @@ wheels = [
[[package]]
name = "requests"
-version = "2.33.0"
+version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -343,9 +345,9 @@ dependencies = [
{ name = "idna" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
@@ -394,11 +396,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.6.3"
+version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
@@ -429,7 +431,7 @@ wheels = [
[[package]]
name = "virtualenv"
-version = "21.2.4"
+version = "21.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
@@ -437,38 +439,51 @@ dependencies = [
{ name = "platformdirs" },
{ name = "python-discovery" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" },
]
[[package]]
name = "wrapt"
-version = "2.1.2"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" },
+ { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" },
+ { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" },
+ { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" },
+ { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" },
+ { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" },
+ { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" },
+]
+
+[[package]]
+name = "wxpython"
+version = "4.2.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/43/81657a6b126ffc19163500a8184d683cec08eb4e1d06905cd0c371c702d0/wxpython-4.2.5.tar.gz", hash = "sha256:44e836d1bccd99c38790bb034b6ecf70d9060f6734320560f7c4b0d006144793", size = 58732217, upload-time = "2026-02-08T20:40:42.086Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" },
- { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" },
- { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" },
- { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" },
- { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" },
- { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" },
- { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" },
- { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" },
- { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" },
- { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" },
- { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" },
- { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" },
- { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" },
- { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" },
- { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" },
- { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" },
- { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" },
- { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" },
- { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" },
- { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" },
- { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" },
- { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" },
- { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b9/1fee711ad5c26e7bbd4e10fae14b2ce0499684a084c3c60169373496ceae/wxpython-4.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f7ec6b028e8b1c4cad1ecb5c8402c2cae7840a25758be0fc209e56df86d1cac", size = 17775906, upload-time = "2026-02-08T20:40:12.031Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/53/521a79cbb169ab6b123e79ea23ebafe3046c1999a431b6fc864f1217bb86/wxpython-4.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:77ac5335d8e4aae92732fc039df24a58181cdfb5bc7931692f1f9415e9eeee7d", size = 18857767, upload-time = "2026-02-08T20:40:14.486Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ea/a69ad0a1e7b01876619982b6cf8db0e26d0f3776b9be73b61d9f0662a2ce/wxpython-4.2.5-cp313-cp313-win32.whl", hash = "sha256:0985f190565b94635f146989886196a7e9faced8911800910460919cb72668cc", size = 14520419, upload-time = "2026-02-08T20:40:17.401Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/bd/d2698369dbc43aa5c9324c23fdd5cd3b23c245861e334b1d976209913f90/wxpython-4.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:3fd3649fc4752f1a02776b7057073c932e5229bbab2031762b01532bcc6bd074", size = 16576741, upload-time = "2026-02-08T20:40:20.646Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/4e/4181734a2bc05940ba4feb3feb2474416b1dc12c329a2ac164632582c4d6/wxpython-4.2.5-cp313-cp313-win_arm64.whl", hash = "sha256:b794d9912464990ea1fd3744fb73fbd7446149e230e5a611ba40eb4ac74755a1", size = 15537287, upload-time = "2026-02-08T20:40:24.658Z" },
]