repdf 0.0.8__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.
- repdf/__init__.py +5 -0
- repdf/__main__.py +5 -0
- repdf/app.py +860 -0
- repdf/assets/check.svg +1 -0
- repdf/assets/partial.svg +1 -0
- repdf/assets/repdf.ico +0 -0
- repdf/assets/repdf.png +0 -0
- repdf/core.py +224 -0
- repdf/entrypoint.py +19 -0
- repdf/icons.py +81 -0
- repdf/packaging_smoke.py +97 -0
- repdf/preferences.py +79 -0
- repdf/render_worker.py +94 -0
- repdf/renderer.py +169 -0
- repdf/strings.py +107 -0
- repdf/theme.py +134 -0
- repdf/widgets.py +276 -0
- repdf/workers.py +61 -0
- repdf-0.0.8.dist-info/METADATA +104 -0
- repdf-0.0.8.dist-info/RECORD +22 -0
- repdf-0.0.8.dist-info/WHEEL +4 -0
- repdf-0.0.8.dist-info/entry_points.txt +3 -0
repdf/renderer.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from repdf.strings import tr
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import secrets
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from collections import OrderedDict
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TextIO
|
|
14
|
+
|
|
15
|
+
from PySide6.QtCore import QObject, Signal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def start_render_backend() -> subprocess.Popen[str]:
|
|
19
|
+
if getattr(sys, "frozen", False):
|
|
20
|
+
return _start_frozen_backend()
|
|
21
|
+
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
22
|
+
process = subprocess.Popen(
|
|
23
|
+
[sys.executable, "-m", "repdf.render_worker", "--server"],
|
|
24
|
+
stdin=subprocess.PIPE,
|
|
25
|
+
stdout=subprocess.PIPE,
|
|
26
|
+
stderr=subprocess.PIPE,
|
|
27
|
+
text=True,
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
bufsize=1,
|
|
30
|
+
creationflags=creation_flags,
|
|
31
|
+
)
|
|
32
|
+
assert process.stdout is not None
|
|
33
|
+
ready_line = process.stdout.readline()
|
|
34
|
+
try:
|
|
35
|
+
ready = json.loads(ready_line)
|
|
36
|
+
except json.JSONDecodeError as exc:
|
|
37
|
+
process.terminate()
|
|
38
|
+
raise RuntimeError(tr('無法啟動 PDF 預覽後端。')) from exc
|
|
39
|
+
if ready.get("type") != "ready":
|
|
40
|
+
process.terminate()
|
|
41
|
+
raise RuntimeError(tr('PDF 預覽後端未正確啟動。'))
|
|
42
|
+
return process
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _start_frozen_backend() -> subprocess.Popen[str]:
|
|
46
|
+
# Windowed executables have no stdin/stdout; use an authenticated local socket.
|
|
47
|
+
token = secrets.token_hex(32)
|
|
48
|
+
with socket.socket() as listener:
|
|
49
|
+
listener.bind(("127.0.0.1", 0))
|
|
50
|
+
listener.listen(1)
|
|
51
|
+
listener.settimeout(30)
|
|
52
|
+
port = listener.getsockname()[1]
|
|
53
|
+
flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
|
54
|
+
process = subprocess.Popen(
|
|
55
|
+
[sys.executable, "--render-server", str(port), token],
|
|
56
|
+
stdin=subprocess.DEVNULL,
|
|
57
|
+
stdout=subprocess.DEVNULL,
|
|
58
|
+
stderr=subprocess.DEVNULL,
|
|
59
|
+
creationflags=flags,
|
|
60
|
+
)
|
|
61
|
+
try:
|
|
62
|
+
connection, _address = listener.accept()
|
|
63
|
+
with connection:
|
|
64
|
+
connection.settimeout(30)
|
|
65
|
+
process.stdin = connection.makefile("w", encoding="utf-8")
|
|
66
|
+
process.stdout = connection.makefile("r", encoding="utf-8")
|
|
67
|
+
ready = json.loads(process.stdout.readline())
|
|
68
|
+
if ready.get("type") != "ready" or ready.get("token") != token:
|
|
69
|
+
raise RuntimeError(tr('PDF 預覽後端驗證失敗。'))
|
|
70
|
+
connection.settimeout(None)
|
|
71
|
+
return process
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
process.terminate()
|
|
74
|
+
process.wait(timeout=5)
|
|
75
|
+
for stream in (process.stdin, process.stdout):
|
|
76
|
+
if stream is not None:
|
|
77
|
+
stream.close()
|
|
78
|
+
raise RuntimeError(tr('無法啟動封裝版 PDF 預覽後端。')) from exc
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RenderService(QObject):
|
|
82
|
+
message = Signal(object)
|
|
83
|
+
|
|
84
|
+
def __init__(self, process: subprocess.Popen[str], parent: QObject | None = None) -> None:
|
|
85
|
+
super().__init__(parent)
|
|
86
|
+
self.process = process
|
|
87
|
+
self._lock = threading.Lock()
|
|
88
|
+
self._busy = False
|
|
89
|
+
self._pending: OrderedDict[int, dict[str, object]] = OrderedDict()
|
|
90
|
+
self._closed = False
|
|
91
|
+
self._reader = threading.Thread(target=self._read_loop, daemon=True)
|
|
92
|
+
self._reader.start()
|
|
93
|
+
|
|
94
|
+
def request(
|
|
95
|
+
self, source: Path, scale: float, output_dir: Path, generation: int,
|
|
96
|
+
*, document_id: int = 0,
|
|
97
|
+
) -> None:
|
|
98
|
+
payload: dict[str, object] = {
|
|
99
|
+
"source": str(source),
|
|
100
|
+
"scale": scale,
|
|
101
|
+
"output_dir": str(output_dir),
|
|
102
|
+
"generation": generation,
|
|
103
|
+
}
|
|
104
|
+
with self._lock:
|
|
105
|
+
if self._closed:
|
|
106
|
+
return
|
|
107
|
+
self._pending[document_id] = payload
|
|
108
|
+
self._pending.move_to_end(document_id, last=False)
|
|
109
|
+
self._start_next()
|
|
110
|
+
|
|
111
|
+
def cancel_pending(self, document_id: int) -> None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
self._pending.pop(document_id, None)
|
|
114
|
+
|
|
115
|
+
def prioritize(self, document_id: int) -> None:
|
|
116
|
+
with self._lock:
|
|
117
|
+
if document_id in self._pending:
|
|
118
|
+
self._pending.move_to_end(document_id, last=False)
|
|
119
|
+
|
|
120
|
+
def _start_next(self) -> None:
|
|
121
|
+
while self._pending and not self._busy and not self._closed:
|
|
122
|
+
_, payload = self._pending.popitem(last=False)
|
|
123
|
+
self._busy = self._write(payload)
|
|
124
|
+
|
|
125
|
+
def _write(self, payload: dict[str, object]) -> bool:
|
|
126
|
+
stream = self.process.stdin
|
|
127
|
+
if stream is None or self.process.poll() is not None:
|
|
128
|
+
self.message.emit({
|
|
129
|
+
"type": "error", "generation": payload["generation"],
|
|
130
|
+
"message": tr('PDF 預覽後端已停止。'),
|
|
131
|
+
})
|
|
132
|
+
return False
|
|
133
|
+
try:
|
|
134
|
+
stream.write(json.dumps(payload) + "\n")
|
|
135
|
+
stream.flush()
|
|
136
|
+
return True
|
|
137
|
+
except (OSError, ValueError) as exc:
|
|
138
|
+
self.message.emit({"type": "error", "generation": payload["generation"],
|
|
139
|
+
"message": str(exc)})
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
def _read_loop(self) -> None:
|
|
143
|
+
stream: TextIO | None = self.process.stdout
|
|
144
|
+
if stream is None:
|
|
145
|
+
return
|
|
146
|
+
for line in stream:
|
|
147
|
+
try:
|
|
148
|
+
payload = json.loads(line)
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
continue
|
|
151
|
+
self.message.emit(payload)
|
|
152
|
+
if payload.get("type") in {"done", "error"}:
|
|
153
|
+
with self._lock:
|
|
154
|
+
self._busy = False
|
|
155
|
+
self._start_next()
|
|
156
|
+
|
|
157
|
+
def close(self) -> None:
|
|
158
|
+
with self._lock:
|
|
159
|
+
self._closed = True
|
|
160
|
+
self._pending.clear()
|
|
161
|
+
if self.process.poll() is None:
|
|
162
|
+
self.process.terminate()
|
|
163
|
+
try:
|
|
164
|
+
self.process.wait(timeout=2)
|
|
165
|
+
except subprocess.TimeoutExpired:
|
|
166
|
+
self.process.kill()
|
|
167
|
+
for stream in (self.process.stdin, self.process.stdout, self.process.stderr):
|
|
168
|
+
if stream is not None:
|
|
169
|
+
stream.close()
|
repdf/strings.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""UI translations shared by the GUI and background error messages."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from string import Formatter
|
|
5
|
+
|
|
6
|
+
LANGUAGE = "zh"
|
|
7
|
+
ENGLISH = {
|
|
8
|
+
"勾選此檔案以加入重組或拆分;未勾選任何標籤時僅輸出目前檔案": "Include this file in merge or split; if no tabs are checked, use only the current file",
|
|
9
|
+
"關閉此檔案": "Close this file",
|
|
10
|
+
"正在讀取 PDF…": "Reading PDF…",
|
|
11
|
+
"已調整第 {p0} 頁的預覽與輸出順序": "Updated the preview and output order of page {p0}",
|
|
12
|
+
"正在產生頁面預覽…": "Rendering page previews…",
|
|
13
|
+
"已載入 {p0} 頁;已選取 {p1} 頁": "Loaded {p0} pages; {p1} selected",
|
|
14
|
+
"部分頁面無法產生預覽": "Some page previews could not be rendered",
|
|
15
|
+
"預覽失敗": "Preview failed",
|
|
16
|
+
"無法完成頁面預覽:\n{p0}": "Could not render page previews:\n{p0}",
|
|
17
|
+
"請先開啟 PDF 檔案,或將多個 PDF 拖曳到視窗": "Open PDF files, or drag PDFs into this window",
|
|
18
|
+
"就緒": "Ready",
|
|
19
|
+
"檔案與顯示": "Files and display",
|
|
20
|
+
"開啟 PDF": "Open PDF",
|
|
21
|
+
"開啟 PDF(Ctrl+O)": "Open PDF (Ctrl+O)",
|
|
22
|
+
"輸出選取頁面": "Merge selected pages",
|
|
23
|
+
"輸出選取頁面為新 PDF(Ctrl+S / Ctrl+Shift+S)": "Merge selected pages into a new PDF (Ctrl+S / Ctrl+Shift+S)",
|
|
24
|
+
"拆分選取頁面": "Split selected pages",
|
|
25
|
+
"拆分選取頁面:依目前順序存成單頁 PDF,檔名附加補零序號": "Split selected pages into individual PDFs in the current order, with zero-padded file numbers",
|
|
26
|
+
"單數頁": "Odd pages",
|
|
27
|
+
"雙數頁": "Even pages",
|
|
28
|
+
"選取或取消原始頁碼的單數頁": "Select or deselect odd source page numbers in the current tab",
|
|
29
|
+
"選取或取消原始頁碼的雙數頁": "Select or deselect even source page numbers in the current tab",
|
|
30
|
+
"縮小": "Zoom out",
|
|
31
|
+
"縮小(Ctrl+-)": "Zoom out (Ctrl+-)",
|
|
32
|
+
"頁面顯示比例": "Page zoom",
|
|
33
|
+
"100% 以 96 DPI 顯示 PDF 頁面": "100% displays PDF pages at 96 DPI",
|
|
34
|
+
"放大": "Zoom in",
|
|
35
|
+
"放大(Ctrl++ / Ctrl+=)": "Zoom in (Ctrl++ / Ctrl+=)",
|
|
36
|
+
"頁面顯示比例:25%–200%\n拖曳右側滑桿調整比例。\n放大:Ctrl++ 或 Ctrl+=(每次增加 25 個百分點)\n縮小:Ctrl+-(每次減少 25 個百分點)\n回到 100%:點擊比例或按 Ctrl+0\n100% 以 96 DPI 顯示 PDF 頁面。": "Page zoom: 25%–200%\nDrag the slider on the right to adjust zoom.\nZoom in: Ctrl++ or Ctrl+= (25 percentage points per step)\nZoom out: Ctrl+- (25 percentage points per step)\nReset to 100%: click the percentage or press Ctrl+0\n100% displays PDF pages at 96 DPI.",
|
|
37
|
+
"repdf — PDF 選頁重組工具 {p0} 版": "repdf {p0}",
|
|
38
|
+
"目前檔案:已選取 {p0} / {p1} 頁;輸出合計 {p2} 頁": "Current file: {p0} / {p1} pages selected; {p2} pages in output",
|
|
39
|
+
"PDF 檔案 (*.pdf)": "PDF files (*.pdf)",
|
|
40
|
+
"部分 PDF 無法開啟": "Some PDFs could not be opened",
|
|
41
|
+
"拆分選取頁面:指定檔名,將自動附加序號": "Split selected pages: choose a base filename; numbers will be appended",
|
|
42
|
+
"無法輸出": "Cannot export",
|
|
43
|
+
"輸出檔案不可覆蓋來源 PDF。": "Output files must not overwrite a source PDF.",
|
|
44
|
+
"覆蓋單頁檔案": "Overwrite single-page files",
|
|
45
|
+
"已有 {p0} 個同名單頁檔案,例如:\n{p1}\n\n是否覆蓋?": "{p0} single-page files already exist, for example:\n{p1}\n\nOverwrite them?",
|
|
46
|
+
"正在拆分單頁 PDF…": "Exporting single-page PDFs…",
|
|
47
|
+
"正在輸出 PDF…": "Exporting PDF…",
|
|
48
|
+
"輸出完成:{p0}": "Export complete: {p0}",
|
|
49
|
+
"已拆分為 {p0} 個單頁 PDF:\n{p1}": "Exported {p0} single-page PDFs:\n{p1}",
|
|
50
|
+
"已輸出 {p0} 頁至:\n{p1}": "Exported {p0} pages to:\n{p1}",
|
|
51
|
+
"輸出完成": "Export complete",
|
|
52
|
+
"輸出失敗": "Export failed",
|
|
53
|
+
"repdf — PDF 選頁重組工具": "repdf",
|
|
54
|
+
"拖曳到另一頁可調整順序": "Drag to another page to change the order",
|
|
55
|
+
"第 {p0} 頁": "Page {p0}",
|
|
56
|
+
"☰ 拖曳排序": "☰ Reorder",
|
|
57
|
+
"載入中…": "Loading…",
|
|
58
|
+
"無法產生預覽": "Preview unavailable",
|
|
59
|
+
"找不到來源 PDF 檔案:{p0}": "Source PDF not found: {p0}",
|
|
60
|
+
"目前不支援密碼保護的 PDF:{p0}": "Password-protected PDFs are not supported: {p0}",
|
|
61
|
+
"選取的頁碼超出來源 PDF 範圍:{p0}": "Selected page numbers are outside the source PDF: {p0}",
|
|
62
|
+
"請至少選取一頁。": "Select at least one page.",
|
|
63
|
+
"部分單頁檔案已存在,請確認覆蓋或更換檔名。": "Some single-page files already exist. Confirm overwrite or choose another filename.",
|
|
64
|
+
"拆分輸出失敗;已完成 {p0} / {p1} 個檔案。\n{p2}": "Split export failed; {p0} / {p1} files completed.\n{p2}",
|
|
65
|
+
"找不到來源 PDF 檔案。": "Source PDF not found.",
|
|
66
|
+
"目前不支援密碼保護的 PDF。": "Password-protected PDFs are not supported.",
|
|
67
|
+
"無法讀取這個 PDF,檔案可能已損壞或格式不受支援。": "Could not read this PDF. It may be damaged or use an unsupported format.",
|
|
68
|
+
"這個 PDF 沒有可用的頁面。": "This PDF has no pages.",
|
|
69
|
+
"無法啟動 PDF 預覽後端。": "Could not start the PDF preview backend.",
|
|
70
|
+
"PDF 預覽後端未正確啟動。": "The PDF preview backend did not start correctly.",
|
|
71
|
+
"PDF 預覽後端驗證失敗。": "PDF preview backend authentication failed.",
|
|
72
|
+
"無法啟動封裝版 PDF 預覽後端。": "Could not start the packaged PDF preview backend.",
|
|
73
|
+
"PDF 預覽後端已停止。": "The PDF preview backend has stopped.",
|
|
74
|
+
"跟隨系統": "Follow system",
|
|
75
|
+
"中文": "Chinese",
|
|
76
|
+
"淺色": "Light",
|
|
77
|
+
"深色": "Dark",
|
|
78
|
+
"介面語言": "Interface language",
|
|
79
|
+
"介面配色": "Color theme",
|
|
80
|
+
"介面語言:{mode}(目前:{language})\n點擊依序切換:跟隨系統 → 中文 → English": "Interface language: {mode} (current: {language})\nClick to cycle: Follow system → Chinese → English",
|
|
81
|
+
"介面配色:{mode}\n點擊依序切換:跟隨系統 → 淺色 → 深色": "Color theme: {mode}\nClick to cycle: Follow system → Light → Dark",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def tr(source: str, **values) -> str:
|
|
86
|
+
return (ENGLISH.get(source, source) if LANGUAGE == "en" else source).format(**values)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _pattern(template):
|
|
90
|
+
parts = []
|
|
91
|
+
for literal, field, _, _ in Formatter().parse(template):
|
|
92
|
+
parts.append(re.escape(literal))
|
|
93
|
+
if field is not None:
|
|
94
|
+
parts.append(f"(?P<{field}>.*?)")
|
|
95
|
+
return re.compile("".join(parts), re.DOTALL)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_PATTERNS = [(zh, en, _pattern(zh), _pattern(en)) for zh, en in ENGLISH.items()]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def localize_text(text: str) -> str:
|
|
102
|
+
"""Retranslate existing text without changing filenames or user-provided values."""
|
|
103
|
+
for zh, en, zh_pattern, en_pattern in _PATTERNS:
|
|
104
|
+
match = zh_pattern.fullmatch(text) or en_pattern.fullmatch(text)
|
|
105
|
+
if match:
|
|
106
|
+
return (en if LANGUAGE == "en" else zh).format(**match.groupdict())
|
|
107
|
+
return text
|
repdf/theme.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QObject, Qt, Slot
|
|
6
|
+
from PySide6.QtGui import QColor, QGuiApplication, QPalette
|
|
7
|
+
from PySide6.QtWidgets import QApplication
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThemeController(QObject):
|
|
11
|
+
def __init__(self, app: QApplication, mode: str = "system", parent=None) -> None:
|
|
12
|
+
super().__init__(parent or app)
|
|
13
|
+
self.app = app
|
|
14
|
+
self.mode = mode if mode in {"system", "light", "dark"} else "system"
|
|
15
|
+
hints = QGuiApplication.styleHints()
|
|
16
|
+
if hasattr(hints, "colorSchemeChanged"):
|
|
17
|
+
hints.colorSchemeChanged.connect(self.apply)
|
|
18
|
+
self.apply()
|
|
19
|
+
|
|
20
|
+
def set_mode(self, mode: str) -> None:
|
|
21
|
+
if mode not in {"system", "light", "dark"}:
|
|
22
|
+
raise ValueError(mode)
|
|
23
|
+
self.mode = mode
|
|
24
|
+
self.apply()
|
|
25
|
+
|
|
26
|
+
@Slot()
|
|
27
|
+
def apply(self, *_args: object) -> None:
|
|
28
|
+
scheme = QGuiApplication.styleHints().colorScheme()
|
|
29
|
+
dark = self.mode == "dark" or (self.mode == "system" and scheme == Qt.ColorScheme.Dark)
|
|
30
|
+
palette = QPalette()
|
|
31
|
+
colors = (
|
|
32
|
+
{
|
|
33
|
+
QPalette.ColorRole.Window: "#202124",
|
|
34
|
+
QPalette.ColorRole.WindowText: "#f1f3f4",
|
|
35
|
+
QPalette.ColorRole.Base: "#17181a",
|
|
36
|
+
QPalette.ColorRole.AlternateBase: "#292a2d",
|
|
37
|
+
QPalette.ColorRole.Text: "#f1f3f4",
|
|
38
|
+
QPalette.ColorRole.Button: "#303134",
|
|
39
|
+
QPalette.ColorRole.ButtonText: "#f1f3f4",
|
|
40
|
+
QPalette.ColorRole.Highlight: "#8ab4f8",
|
|
41
|
+
QPalette.ColorRole.HighlightedText: "#202124",
|
|
42
|
+
QPalette.ColorRole.PlaceholderText: "#9aa0a6",
|
|
43
|
+
}
|
|
44
|
+
if dark
|
|
45
|
+
else {
|
|
46
|
+
QPalette.ColorRole.Window: "#f5f6f8",
|
|
47
|
+
QPalette.ColorRole.WindowText: "#202124",
|
|
48
|
+
QPalette.ColorRole.Base: "#ffffff",
|
|
49
|
+
QPalette.ColorRole.AlternateBase: "#eef0f3",
|
|
50
|
+
QPalette.ColorRole.Text: "#202124",
|
|
51
|
+
QPalette.ColorRole.Button: "#ffffff",
|
|
52
|
+
QPalette.ColorRole.ButtonText: "#202124",
|
|
53
|
+
QPalette.ColorRole.Highlight: "#0b57d0",
|
|
54
|
+
QPalette.ColorRole.HighlightedText: "#ffffff",
|
|
55
|
+
QPalette.ColorRole.PlaceholderText: "#6f7378",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
for role, color in colors.items():
|
|
59
|
+
palette.setColor(role, QColor(color))
|
|
60
|
+
palette.setColor(
|
|
61
|
+
QPalette.ColorGroup.Disabled,
|
|
62
|
+
QPalette.ColorRole.ButtonText,
|
|
63
|
+
QColor("#777b80" if dark else "#9aa0a6"),
|
|
64
|
+
)
|
|
65
|
+
self.app.setPalette(palette)
|
|
66
|
+
border = "#4a4d51" if dark else "#d5d8dc"
|
|
67
|
+
card = "#292a2d" if dark else "#ffffff"
|
|
68
|
+
slider_track = "#5f6368" if dark else "#c4c7c5"
|
|
69
|
+
slider_fill = "#8ab4f8" if dark else "#0b57d0"
|
|
70
|
+
slider_handle = "#aecbfa" if dark else "#0b57d0"
|
|
71
|
+
hover = "#35383d" if dark else "#e7ebf0"
|
|
72
|
+
pressed = "#41464e" if dark else "#d8e3f3"
|
|
73
|
+
disabled = "#777b80" if dark else "#9aa0a6"
|
|
74
|
+
check = files("repdf").joinpath("assets", "check.svg").as_posix()
|
|
75
|
+
partial = files("repdf").joinpath("assets", "partial.svg").as_posix()
|
|
76
|
+
self.app.setStyleSheet(
|
|
77
|
+
f"""
|
|
78
|
+
QFrame#pageCard {{ background: {card}; border: 1px solid {border};
|
|
79
|
+
border-radius: 8px; }}
|
|
80
|
+
QFrame#pageCard QLabel, QFrame#pageCard QCheckBox {{ border: none; }}
|
|
81
|
+
QToolBar {{ background: palette(window); border: none;
|
|
82
|
+
border-bottom: 1px solid {border}; padding: 4px 8px; spacing: 4px; }}
|
|
83
|
+
QToolBar::separator {{ background: {border}; width: 1px; margin: 5px 4px; }}
|
|
84
|
+
QTabWidget::pane {{ border: none; }}
|
|
85
|
+
QTabBar::tab {{ background: transparent; color: palette(window-text);
|
|
86
|
+
border: none; border-bottom: 2px solid transparent;
|
|
87
|
+
padding: 5px 8px; margin-right: 2px; }}
|
|
88
|
+
QTabBar::tab:selected {{ background: {card}; border-bottom-color: {slider_fill}; }}
|
|
89
|
+
QTabBar::tab:!selected:hover {{ background: {hover}; }}
|
|
90
|
+
QToolButton {{ background: transparent; border: 1px solid transparent;
|
|
91
|
+
border-radius: 4px; padding: 4px; color: palette(button-text); }}
|
|
92
|
+
QToolButton:hover {{ background: {hover}; }}
|
|
93
|
+
QToolButton:pressed {{ background: {pressed}; }}
|
|
94
|
+
QToolButton:focus {{ border-color: {slider_fill}; }}
|
|
95
|
+
QToolButton:disabled {{ color: {disabled}; background: transparent; }}
|
|
96
|
+
QPushButton {{ background: {card}; color: palette(button-text);
|
|
97
|
+
border: 1px solid {border}; border-radius: 5px;
|
|
98
|
+
min-height: 30px; padding: 2px 12px; }}
|
|
99
|
+
QPushButton:hover {{ background: {hover}; border-color: {slider_fill}; }}
|
|
100
|
+
QPushButton:pressed {{ background: {pressed}; }}
|
|
101
|
+
QPushButton:focus {{ border-color: {slider_fill}; }}
|
|
102
|
+
QPushButton:disabled {{ color: {disabled}; }}
|
|
103
|
+
QCheckBox {{ spacing: 6px; }}
|
|
104
|
+
QCheckBox::indicator {{ width: 16px; height: 16px;
|
|
105
|
+
border: 1px solid {border}; border-radius: 3px;
|
|
106
|
+
background: {card}; }}
|
|
107
|
+
QCheckBox::indicator:hover {{ border-color: {slider_fill}; }}
|
|
108
|
+
QCheckBox::indicator:checked {{ background: #0b57d0; border-color: #0b57d0;
|
|
109
|
+
image: url("{check}"); }}
|
|
110
|
+
QCheckBox::indicator:indeterminate {{ background: #0b57d0; border-color: #0b57d0;
|
|
111
|
+
image: url("{partial}"); }}
|
|
112
|
+
QCheckBox::indicator:disabled {{ background: {hover}; border-color: {border}; }}
|
|
113
|
+
QScrollArea {{ border: 1px solid {border}; border-radius: 6px; }}
|
|
114
|
+
QStatusBar {{ background: palette(window); border: none; }}
|
|
115
|
+
QStatusBar::item {{ border: none; }}
|
|
116
|
+
QScrollBar:vertical {{ background: transparent; width: 10px; margin: 2px; }}
|
|
117
|
+
QScrollBar:horizontal {{ background: transparent; height: 10px; margin: 2px; }}
|
|
118
|
+
QScrollBar::handle {{ background: {slider_track}; border-radius: 3px;
|
|
119
|
+
min-width: 24px; min-height: 24px; }}
|
|
120
|
+
QScrollBar::handle:hover {{ background: {slider_fill}; }}
|
|
121
|
+
QScrollBar::add-line, QScrollBar::sub-line {{ width: 0; height: 0; border: none; }}
|
|
122
|
+
QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }}
|
|
123
|
+
QSlider::groove:horizontal {{ height: 2px; background: {slider_track};
|
|
124
|
+
border-radius: 1px; }}
|
|
125
|
+
QSlider::sub-page:horizontal {{ background: {slider_fill};
|
|
126
|
+
border-radius: 1px; }}
|
|
127
|
+
QSlider::add-page:horizontal {{ background: {slider_track};
|
|
128
|
+
border-radius: 1px; }}
|
|
129
|
+
QSlider::handle:horizontal {{ background: {slider_handle};
|
|
130
|
+
border: none;
|
|
131
|
+
border-radius: 5px; width: 10px;
|
|
132
|
+
margin: -4px 0; }}
|
|
133
|
+
"""
|
|
134
|
+
)
|