subtle-gui 26.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- subtle/__init__.py +184 -0
- subtle/common.py +178 -0
- subtle/config.py +291 -0
- subtle/constants.py +49 -0
- subtle/core/icons.py +116 -0
- subtle/core/media.py +254 -0
- subtle/core/mediafile.py +148 -0
- subtle/core/mkvextract.py +89 -0
- subtle/core/spellcheck.py +219 -0
- subtle/formats/base.py +187 -0
- subtle/formats/pgssubs.py +551 -0
- subtle/formats/srtsubs.py +146 -0
- subtle/formats/ssasubs.py +166 -0
- subtle/gui/filetree.py +88 -0
- subtle/gui/highlighter.py +104 -0
- subtle/gui/imageviewer.py +89 -0
- subtle/gui/mediaview.py +260 -0
- subtle/gui/subsview.py +188 -0
- subtle/gui/texteditor.py +339 -0
- subtle/gui/toolspanel.py +260 -0
- subtle/guimain.py +153 -0
- subtle/ocr/base.py +41 -0
- subtle/ocr/tesseract.py +135 -0
- subtle/shared.py +103 -0
- subtle_gui-26.1.0.dist-info/METADATA +66 -0
- subtle_gui-26.1.0.dist-info/RECORD +30 -0
- subtle_gui-26.1.0.dist-info/WHEEL +5 -0
- subtle_gui-26.1.0.dist-info/entry_points.txt +2 -0
- subtle_gui-26.1.0.dist-info/licenses/LICENSE +674 -0
- subtle_gui-26.1.0.dist-info/top_level.txt +1 -0
subtle/__init__.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subtle – Init File
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
This file is a part of Subtle
|
|
6
|
+
Copyright (C) Veronica Berglyd Olsen
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful, but
|
|
14
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
16
|
+
General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import getopt
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
from subtle.config import Config
|
|
31
|
+
from subtle.shared import SharedData
|
|
32
|
+
|
|
33
|
+
from PyQt6.QtWidgets import QApplication
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
36
|
+
from subtle.guimain import GuiMain
|
|
37
|
+
|
|
38
|
+
# Package Meta
|
|
39
|
+
# ============
|
|
40
|
+
|
|
41
|
+
__package__ = "subtle-gui"
|
|
42
|
+
__copyright__ = "Copyright (C) Veronica Berglyd Olsen"
|
|
43
|
+
__license__ = "GPLv3"
|
|
44
|
+
__author__ = "Veronica Berglyd Olsen"
|
|
45
|
+
__maintainer__ = "Veronica Berglyd Olsen"
|
|
46
|
+
__email__ = "code@vkbo.net"
|
|
47
|
+
__version__ = "26.1.0"
|
|
48
|
+
__date__ = "2026-06-24"
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Main Program
|
|
55
|
+
##
|
|
56
|
+
|
|
57
|
+
CONFIG = Config()
|
|
58
|
+
SHARED = SharedData()
|
|
59
|
+
|
|
60
|
+
# ANSI Colours
|
|
61
|
+
RED = "\033[91m"
|
|
62
|
+
GREEN = "\033[92m"
|
|
63
|
+
YELLOW = "\033[93m"
|
|
64
|
+
BLUE = "\033[94m"
|
|
65
|
+
WHITE = "\033[97m"
|
|
66
|
+
END = "\033[0m"
|
|
67
|
+
|
|
68
|
+
# Log Format Components
|
|
69
|
+
TIME = "[{asctime:}]"
|
|
70
|
+
FILE = "{filename:>18}"
|
|
71
|
+
LINE = "{lineno:<4d}"
|
|
72
|
+
LVLP = "{levelname:8}"
|
|
73
|
+
LVLC = "{levelname:17}"
|
|
74
|
+
TEXT = "{message:}"
|
|
75
|
+
|
|
76
|
+
# Read Environment
|
|
77
|
+
FORCE_COLOR = bool(os.environ.get("FORCE_COLOR"))
|
|
78
|
+
NO_COLOR = bool(os.environ.get("NO_COLOR"))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main(sysArgs: list | None = None) -> GuiMain | None:
|
|
82
|
+
"""Parse command line, set up logging, and launch main GUI."""
|
|
83
|
+
if sysArgs is None:
|
|
84
|
+
sysArgs = sys.argv[1:]
|
|
85
|
+
|
|
86
|
+
# Valid Input Options
|
|
87
|
+
shortOpt = "hvicd"
|
|
88
|
+
longOpt = [
|
|
89
|
+
"help",
|
|
90
|
+
"version",
|
|
91
|
+
"info",
|
|
92
|
+
"debug",
|
|
93
|
+
"color",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
helpMsg = (
|
|
97
|
+
f"Subtle {__version__} ({__date__})\n"
|
|
98
|
+
f"{__copyright__}\n"
|
|
99
|
+
"\n"
|
|
100
|
+
"This program is distributed in the hope that it will be useful,\n"
|
|
101
|
+
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
|
|
102
|
+
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
|
|
103
|
+
"GNU General Public Licence for more details.\n"
|
|
104
|
+
"\n"
|
|
105
|
+
"Usage:\n"
|
|
106
|
+
" -h, --help Print this message.\n"
|
|
107
|
+
" -v, --version Print program version and exit.\n"
|
|
108
|
+
" -i, --info Print additional runtime information.\n"
|
|
109
|
+
" -d, --debug Print debug output. Includes --info.\n"
|
|
110
|
+
" -c, --color Add ANSI colors to log output.\n"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Defaults
|
|
114
|
+
logLevel = logging.WARN
|
|
115
|
+
fmtColor = FORCE_COLOR
|
|
116
|
+
fmtLong = False
|
|
117
|
+
|
|
118
|
+
# Parse Options
|
|
119
|
+
try:
|
|
120
|
+
inOpts, _ = getopt.getopt(sysArgs, shortOpt, longOpt)
|
|
121
|
+
except getopt.GetoptError as exc:
|
|
122
|
+
print(helpMsg)
|
|
123
|
+
print(f"ERROR: {exc!s}")
|
|
124
|
+
sys.exit(2)
|
|
125
|
+
|
|
126
|
+
for inOpt, _ in inOpts:
|
|
127
|
+
if inOpt in ("-h", "--help"):
|
|
128
|
+
print(helpMsg)
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
elif inOpt in ("-v", "--version"):
|
|
131
|
+
print(f"Subtle Version {__version__} [{__date__}]")
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
elif inOpt in ("-i", "--info"):
|
|
134
|
+
logLevel = logging.INFO
|
|
135
|
+
elif inOpt in ("-d", "--debug"):
|
|
136
|
+
fmtLong = True
|
|
137
|
+
logLevel = logging.DEBUG
|
|
138
|
+
elif inOpt in ("-c", "--color"):
|
|
139
|
+
fmtColor = not NO_COLOR
|
|
140
|
+
|
|
141
|
+
if fmtColor:
|
|
142
|
+
# This will overwrite the default level names, and also ensure that
|
|
143
|
+
# they can be converted back to integer levels
|
|
144
|
+
logging.addLevelName(logging.DEBUG, f"{BLUE}DEBUG{END}")
|
|
145
|
+
logging.addLevelName(logging.INFO, f"{GREEN}INFO{END}")
|
|
146
|
+
logging.addLevelName(logging.WARNING, f"{YELLOW}WARNING{END}")
|
|
147
|
+
logging.addLevelName(logging.ERROR, f"{RED}ERROR{END}")
|
|
148
|
+
logging.addLevelName(logging.CRITICAL, f"{RED}CRITICAL{END}")
|
|
149
|
+
|
|
150
|
+
logTxt = f"{LVLC} {TEXT}" if fmtColor else f"{LVLP} {TEXT}"
|
|
151
|
+
logPos = f"{BLUE}{FILE}{END}:{WHITE}{LINE}{END}" if fmtColor else f"{FILE}:{LINE}"
|
|
152
|
+
logFmt = f"{TIME} {logPos} {logTxt}" if fmtLong else logTxt
|
|
153
|
+
|
|
154
|
+
# Setup Logging
|
|
155
|
+
pkgLogger = logging.getLogger(__package__)
|
|
156
|
+
pkgLogger.setLevel(logLevel)
|
|
157
|
+
if len(pkgLogger.handlers) == 0:
|
|
158
|
+
# Make sure we only create one logger (mostly an issue with tests)
|
|
159
|
+
cHandle = logging.StreamHandler()
|
|
160
|
+
cHandle.setFormatter(logging.Formatter(fmt=logFmt, style="{"))
|
|
161
|
+
pkgLogger.addHandler(cHandle)
|
|
162
|
+
|
|
163
|
+
logger.info("Starting Subtle %s (%s)", __version__, __date__)
|
|
164
|
+
|
|
165
|
+
# Finish initialising config
|
|
166
|
+
CONFIG.initialise()
|
|
167
|
+
|
|
168
|
+
from subtle.guimain import GuiMain
|
|
169
|
+
|
|
170
|
+
app = QApplication([CONFIG.appName])
|
|
171
|
+
app.setApplicationName(CONFIG.appName)
|
|
172
|
+
app.setApplicationVersion(__version__)
|
|
173
|
+
app.setDesktopFileName(CONFIG.appName)
|
|
174
|
+
|
|
175
|
+
# Run Config steps that require the QApplication
|
|
176
|
+
CONFIG.load()
|
|
177
|
+
CONFIG.fonts(app)
|
|
178
|
+
CONFIG.localisation(app)
|
|
179
|
+
|
|
180
|
+
# Launch main GUI
|
|
181
|
+
gui = GuiMain()
|
|
182
|
+
gui.show()
|
|
183
|
+
|
|
184
|
+
sys.exit(app.exec())
|
subtle/common.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subtle – Common Functions
|
|
3
|
+
=========================
|
|
4
|
+
|
|
5
|
+
This file is a part of Subtle
|
|
6
|
+
Copyright (C) Veronica Berglyd Olsen
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful, but
|
|
14
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
16
|
+
General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
import re
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
T_Subs = Literal["SRT"] | Literal["SSA"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def simplified(text: str) -> str:
|
|
37
|
+
"""Take a string and strip leading and trailing whitespaces, and
|
|
38
|
+
replace all occurrences of (multiple) whitespaces with a 0x20 space.
|
|
39
|
+
"""
|
|
40
|
+
return " ".join(str(text).strip().split())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def checkInt(value: Any, default: int) -> int:
|
|
44
|
+
"""Check if a variable is an integer."""
|
|
45
|
+
try:
|
|
46
|
+
return int(value)
|
|
47
|
+
except Exception:
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def formatInt(value: int) -> str:
|
|
52
|
+
"""Formats an integer with k, M, G etc."""
|
|
53
|
+
if not isinstance(value, int):
|
|
54
|
+
return "ERR"
|
|
55
|
+
|
|
56
|
+
fVal = float(value)
|
|
57
|
+
if fVal > 1000.0:
|
|
58
|
+
for pF in ["k", "M", "G", "T", "P", "E"]:
|
|
59
|
+
fVal /= 1000.0
|
|
60
|
+
if fVal < 1000.0:
|
|
61
|
+
if fVal < 10.0:
|
|
62
|
+
return f"{fVal:4.2f}\u202f{pF}"
|
|
63
|
+
elif fVal < 100.0:
|
|
64
|
+
return f"{fVal:4.1f}\u202f{pF}"
|
|
65
|
+
else:
|
|
66
|
+
return f"{fVal:3.0f}\u202f{pF}"
|
|
67
|
+
|
|
68
|
+
return str(value) + "\u202f"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def textCleanup(text: str) -> str:
|
|
72
|
+
"""Do some common cleanup on text strings."""
|
|
73
|
+
return text.replace("--", "\u2014").replace("....", "...")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def regexCleanup(text: str, patterns: list[tuple[re.Pattern, str]]) -> str:
|
|
77
|
+
"""Replaces all occurrences of match group 1 in patterns."""
|
|
78
|
+
for regEx, value in patterns:
|
|
79
|
+
matches = [
|
|
80
|
+
(s, e, value) for match in regEx.finditer(text)
|
|
81
|
+
if (s := match.start(1)) >= 0 and (e := match.end(1)) >= 0
|
|
82
|
+
]
|
|
83
|
+
for s, e, value in reversed(matches):
|
|
84
|
+
text = text[:s] + value + text[e:]
|
|
85
|
+
return text
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def closeItalics(text: list[str]) -> list[str]:
|
|
89
|
+
"""Make sure italics doesn't span multiple lines."""
|
|
90
|
+
italics = False
|
|
91
|
+
result = []
|
|
92
|
+
for line in text:
|
|
93
|
+
if italics:
|
|
94
|
+
line = f"<i>{line}"
|
|
95
|
+
if line.count("<i>") - line.count("</i>") > 0:
|
|
96
|
+
italics = True
|
|
97
|
+
line = f"{line}</i>"
|
|
98
|
+
result.append(line)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
|
|
103
|
+
"""Encode a dictionary, list or tuple as a json object or array, and
|
|
104
|
+
indent from level n up to a max level nmax if nmax is larger than 0.
|
|
105
|
+
"""
|
|
106
|
+
if not isinstance(data, (dict, list, tuple)):
|
|
107
|
+
return "[]"
|
|
108
|
+
|
|
109
|
+
buffer = []
|
|
110
|
+
indent = ""
|
|
111
|
+
|
|
112
|
+
for chunk in json.JSONEncoder().iterencode(data):
|
|
113
|
+
if chunk == "": # pragma: no cover
|
|
114
|
+
# Just a precaution
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
first = chunk[0]
|
|
118
|
+
if chunk in ("{}", "[]"):
|
|
119
|
+
buffer.append(chunk)
|
|
120
|
+
|
|
121
|
+
elif first in ("{", "["):
|
|
122
|
+
n += 1
|
|
123
|
+
indent = "\n"+" "*n
|
|
124
|
+
if n > nmax and nmax > 0:
|
|
125
|
+
buffer.append(chunk)
|
|
126
|
+
else:
|
|
127
|
+
buffer.append(chunk[0] + indent + chunk[1:])
|
|
128
|
+
|
|
129
|
+
elif first in ("}", "]"):
|
|
130
|
+
n -= 1
|
|
131
|
+
indent = "\n"+" "*n
|
|
132
|
+
if n >= nmax and nmax > 0:
|
|
133
|
+
buffer.append(chunk)
|
|
134
|
+
else:
|
|
135
|
+
buffer.append(indent + chunk)
|
|
136
|
+
|
|
137
|
+
elif first == ",":
|
|
138
|
+
if n > nmax and nmax > 0:
|
|
139
|
+
buffer.append(chunk)
|
|
140
|
+
else:
|
|
141
|
+
buffer.append(chunk[0] + indent + chunk[1:].lstrip())
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
buffer.append(chunk)
|
|
145
|
+
|
|
146
|
+
return "".join(buffer)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def formatTS(value: int) -> str:
|
|
150
|
+
"""Format millisecond integer as HH:MM:SS,uuu timestamp."""
|
|
151
|
+
i, f = value//1000, value%1000
|
|
152
|
+
return f"{i//3600:02d}:{i%3600//60:02d}:{i%60:02d},{f:03d}"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def decodeTS(value: str | None, default: int = 0, fmt: T_Subs = "SRT") -> int:
|
|
156
|
+
"""Decode a SRT time stamp to milliseconds."""
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
if fmt == "SRT" and len(value) >= 12:
|
|
159
|
+
if value[2] == ":" and value[5] == ":" and value[8] in ".,":
|
|
160
|
+
try:
|
|
161
|
+
return (
|
|
162
|
+
3600000*int(value[0:2])
|
|
163
|
+
+ 60000*int(value[3:5])
|
|
164
|
+
+ int(value[6:8] + value[9:12])
|
|
165
|
+
)
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
elif fmt == "SSA" and len(value) == 10:
|
|
169
|
+
if value[1] == ":" and value[4] == ":" and value[7] in ":.,":
|
|
170
|
+
try:
|
|
171
|
+
return (
|
|
172
|
+
3600000*int(value[0])
|
|
173
|
+
+ 60000*int(value[2:4])
|
|
174
|
+
+ 10*int(value[5:7] + value[8:10])
|
|
175
|
+
)
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
return default
|
subtle/config.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subtle – Main Config
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
This file is a part of Subtle
|
|
6
|
+
Copyright (C) Veronica Berglyd Olsen
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful, but
|
|
14
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
16
|
+
General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
from copy import deepcopy
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Literal
|
|
31
|
+
|
|
32
|
+
from subtle.common import jsonEncode
|
|
33
|
+
|
|
34
|
+
from PyQt6.QtCore import (
|
|
35
|
+
PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QSize,
|
|
36
|
+
QStandardPaths, QSysInfo
|
|
37
|
+
)
|
|
38
|
+
from PyQt6.QtGui import QFont
|
|
39
|
+
from PyQt6.QtWidgets import QApplication
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
DEFAULTS: dict = {
|
|
45
|
+
"Sizes": {
|
|
46
|
+
"mainWindow": [900, 600],
|
|
47
|
+
"mainSplit": [300, 300, 300],
|
|
48
|
+
"contentSplit": [300, 300],
|
|
49
|
+
"viewSplit": [200, 200, 200],
|
|
50
|
+
"fileTreeColumns": [],
|
|
51
|
+
"mediaViewColumns": [],
|
|
52
|
+
"subsViewColumns": [],
|
|
53
|
+
},
|
|
54
|
+
"Settings": {
|
|
55
|
+
"tessData": "",
|
|
56
|
+
},
|
|
57
|
+
"Fonts": {
|
|
58
|
+
"guiFont": "",
|
|
59
|
+
"fixedFont": "",
|
|
60
|
+
"subsFont": "",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
T_Fonts = Literal["gui"] | Literal["fixed"] | Literal["subs"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Config:
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
|
|
71
|
+
self._data: dict[str, dict] = deepcopy(DEFAULTS)
|
|
72
|
+
|
|
73
|
+
self.appName = "Subtle"
|
|
74
|
+
self.appHandle = "subtle"
|
|
75
|
+
|
|
76
|
+
# Set Paths
|
|
77
|
+
confRoot = Path(QStandardPaths.writableLocation(
|
|
78
|
+
QStandardPaths.StandardLocation.ConfigLocation)
|
|
79
|
+
)
|
|
80
|
+
cacheRoot = Path(QStandardPaths.writableLocation(
|
|
81
|
+
QStandardPaths.StandardLocation.CacheLocation)
|
|
82
|
+
)
|
|
83
|
+
self._confPath = confRoot.absolute() / self.appHandle # The user config location
|
|
84
|
+
self._cachePath = cacheRoot.absolute() / self.appHandle # The user cache location
|
|
85
|
+
self._homePath = Path.home().absolute() # The user's home directory
|
|
86
|
+
|
|
87
|
+
self._appPath = Path(__file__).parent.absolute()
|
|
88
|
+
self._appRoot = self._appPath.parent
|
|
89
|
+
|
|
90
|
+
self._confFile = self._confPath / "subtle.json"
|
|
91
|
+
|
|
92
|
+
# Fonts
|
|
93
|
+
self.guiFont = QFont()
|
|
94
|
+
self.fixedFont = QFont()
|
|
95
|
+
self.subsFont = QFont()
|
|
96
|
+
|
|
97
|
+
# Check Qt6 Versions
|
|
98
|
+
self.verQtString = QT_VERSION_STR
|
|
99
|
+
self.verQtValue = QT_VERSION
|
|
100
|
+
self.verPyQtString = PYQT_VERSION_STR
|
|
101
|
+
self.verPyQtValue = PYQT_VERSION
|
|
102
|
+
|
|
103
|
+
# Check Python Version
|
|
104
|
+
self.verPyString = sys.version.split()[0]
|
|
105
|
+
|
|
106
|
+
# Check OS Type
|
|
107
|
+
self.osType = sys.platform
|
|
108
|
+
self.osLinux = False
|
|
109
|
+
self.osWindows = False
|
|
110
|
+
self.osDarwin = False
|
|
111
|
+
self.osUnknown = False
|
|
112
|
+
if self.osType.startswith("linux"):
|
|
113
|
+
self.osLinux = True
|
|
114
|
+
elif self.osType.startswith("darwin"):
|
|
115
|
+
self.osDarwin = True
|
|
116
|
+
elif self.osType.startswith(("win32", "cygwin")):
|
|
117
|
+
self.osWindows = True
|
|
118
|
+
else:
|
|
119
|
+
self.osUnknown = True
|
|
120
|
+
|
|
121
|
+
# Other System Info
|
|
122
|
+
self.hostName = QSysInfo.machineHostName()
|
|
123
|
+
self.kernelVer = QSysInfo.kernelVersion()
|
|
124
|
+
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Properties
|
|
129
|
+
##
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def confPath(self) -> Path:
|
|
133
|
+
"""Return the location for config files."""
|
|
134
|
+
self._confPath.mkdir(exist_ok=True)
|
|
135
|
+
return self._confPath
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def dumpPath(self) -> Path:
|
|
139
|
+
"""Return a location for dumping files during a session."""
|
|
140
|
+
path = self._cachePath / "dump"
|
|
141
|
+
path.mkdir(exist_ok=True)
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# Getters
|
|
146
|
+
##
|
|
147
|
+
|
|
148
|
+
def getSize(self, key: str) -> QSize:
|
|
149
|
+
"""Get a size from config."""
|
|
150
|
+
try:
|
|
151
|
+
size = QSize(*self._data["Sizes"][key])
|
|
152
|
+
except Exception:
|
|
153
|
+
size = QSize()
|
|
154
|
+
return size
|
|
155
|
+
|
|
156
|
+
def getSizes(self, key: str) -> list[int]:
|
|
157
|
+
"""Get a list of sizes from config."""
|
|
158
|
+
try:
|
|
159
|
+
size = list(self._data["Sizes"][key])
|
|
160
|
+
except Exception:
|
|
161
|
+
size = []
|
|
162
|
+
return size
|
|
163
|
+
|
|
164
|
+
def getSetting(self, key: str) -> str:
|
|
165
|
+
"""Get a generic string setting."""
|
|
166
|
+
return str(self._data["Settings"].get(key, ""))
|
|
167
|
+
|
|
168
|
+
def assetPath(self, resource: str, kind: str | None = None) -> Path:
|
|
169
|
+
"""Return the path to an asset."""
|
|
170
|
+
path = self._appPath / "assets"
|
|
171
|
+
if kind:
|
|
172
|
+
path /= kind
|
|
173
|
+
return path / resource
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
##
|
|
177
|
+
# Setters
|
|
178
|
+
##
|
|
179
|
+
|
|
180
|
+
def setSize(self, key: str, value: QSize) -> None:
|
|
181
|
+
"""Set a size in config."""
|
|
182
|
+
if isinstance(value, QSize):
|
|
183
|
+
self._data["Sizes"][key] = [value.width(), value.height()]
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
def setSizes(self, key: str, value: list[int]) -> None:
|
|
187
|
+
"""Set a size in config."""
|
|
188
|
+
if isinstance(value, list):
|
|
189
|
+
try:
|
|
190
|
+
self._data["Sizes"][key] = [int(x) for x in value]
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error("Problem when saving sizes list", exc_info=e)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
def setFontSpec(self, target: T_Fonts, font: QFont | str) -> None:
|
|
196
|
+
"""Set the font """
|
|
197
|
+
if isinstance(font, str):
|
|
198
|
+
temp = QFont()
|
|
199
|
+
temp.fromString(font)
|
|
200
|
+
font = temp
|
|
201
|
+
|
|
202
|
+
if target == "gui":
|
|
203
|
+
self.guiFont = font
|
|
204
|
+
self._data["Fonts"]["guiFont"] = font.toString()
|
|
205
|
+
QApplication.setFont(self.guiFont)
|
|
206
|
+
elif target == "fixed":
|
|
207
|
+
self.fixedFont = font
|
|
208
|
+
self._data["Fonts"]["fixedFont"] = font.toString()
|
|
209
|
+
elif target == "subs":
|
|
210
|
+
self.subsFont = font
|
|
211
|
+
self._data["Fonts"]["subsFont"] = font.toString()
|
|
212
|
+
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# Methods
|
|
217
|
+
##
|
|
218
|
+
|
|
219
|
+
def initialise(self) -> None:
|
|
220
|
+
"""Initialise the config."""
|
|
221
|
+
self._confPath.mkdir(exist_ok=True)
|
|
222
|
+
self._cachePath.mkdir(exist_ok=True)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
def cleanup(self) -> None:
|
|
226
|
+
"""Called before exit to clean up cache."""
|
|
227
|
+
path = self._cachePath / "dump"
|
|
228
|
+
if path.exists():
|
|
229
|
+
logger.debug("Clearing session cache")
|
|
230
|
+
shutil.rmtree(path)
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
def localisation(self, app: QApplication) -> None:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
def fonts(self, app: QApplication) -> None:
|
|
237
|
+
"""Set up fonts."""
|
|
238
|
+
if font := self._data["Fonts"].get("guiFont"):
|
|
239
|
+
self.setFontSpec("gui", font)
|
|
240
|
+
else:
|
|
241
|
+
self.setFontSpec("gui", app.font())
|
|
242
|
+
|
|
243
|
+
if font := self._data["Fonts"].get("fixedFont"):
|
|
244
|
+
self.setFontSpec("fixed", font)
|
|
245
|
+
else:
|
|
246
|
+
self.setFontSpec("fixed", QFont("monospace", app.font().pointSize()))
|
|
247
|
+
|
|
248
|
+
if font := self._data["Fonts"].get("subsFont"):
|
|
249
|
+
self.setFontSpec("subs", font)
|
|
250
|
+
else:
|
|
251
|
+
temp = app.font()
|
|
252
|
+
temp.setPointSizeF(3.0*temp.pointSizeF())
|
|
253
|
+
self.setFontSpec("subs", temp)
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
def load(self) -> None:
|
|
257
|
+
"""Load the app config."""
|
|
258
|
+
if self._confFile.is_file():
|
|
259
|
+
try:
|
|
260
|
+
logger.debug("Loading config")
|
|
261
|
+
with open(self._confFile, mode="r", encoding="utf-8") as fo:
|
|
262
|
+
data = json.load(fo)
|
|
263
|
+
self._storeConfigGroup(data, "Sizes")
|
|
264
|
+
self._storeConfigGroup(data, "Settings")
|
|
265
|
+
self._storeConfigGroup(data, "Fonts")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error("Could not load config", exc_info=e)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
def save(self) -> None:
|
|
271
|
+
"""Save the app config."""
|
|
272
|
+
try:
|
|
273
|
+
logger.debug("Saving config")
|
|
274
|
+
with open(self._confFile, mode="w+", encoding="utf-8") as fo:
|
|
275
|
+
fo.write(jsonEncode(self._data, nmax=2))
|
|
276
|
+
except Exception:
|
|
277
|
+
logger.error("Could not save config")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
##
|
|
281
|
+
# Internal Functions
|
|
282
|
+
##
|
|
283
|
+
|
|
284
|
+
def _storeConfigGroup(self, data: dict, group: str) -> None:
|
|
285
|
+
"""Process a group from config and save the data."""
|
|
286
|
+
if isinstance(data, dict) and group in self._data:
|
|
287
|
+
loaded = data.get(group, {})
|
|
288
|
+
default = DEFAULTS.get(group, {})
|
|
289
|
+
values = {k: v for k, v in loaded.items() if k in default}
|
|
290
|
+
self._data[group].update(values)
|
|
291
|
+
return
|
subtle/constants.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subtle – Constants
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
This file is a part of Subtle
|
|
6
|
+
Copyright (C) Veronica Berglyd Olsen
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful, but
|
|
14
|
+
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
16
|
+
General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import Final
|
|
25
|
+
|
|
26
|
+
from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def trConst(text: str) -> str:
|
|
30
|
+
"""Wrapper function for locally translating constants."""
|
|
31
|
+
return QCoreApplication.translate("Constant", text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MediaType(Enum):
|
|
35
|
+
|
|
36
|
+
VIDEO = 0
|
|
37
|
+
AUDIO = 1
|
|
38
|
+
SUBS = 2
|
|
39
|
+
OTHER = 4
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GuiLabels:
|
|
43
|
+
|
|
44
|
+
MEDIA_TYPES: Final[dict[MediaType, str]] = {
|
|
45
|
+
MediaType.VIDEO: QT_TRANSLATE_NOOP("Constant", "Video"),
|
|
46
|
+
MediaType.AUDIO: QT_TRANSLATE_NOOP("Constant", "Audio"),
|
|
47
|
+
MediaType.SUBS: QT_TRANSLATE_NOOP("Constant", "Subtitles"),
|
|
48
|
+
MediaType.OTHER: QT_TRANSLATE_NOOP("Constant", "Other"),
|
|
49
|
+
}
|