pdf-tool-cli 1.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.
- pdf_tool_cli-1.1.0.dist-info/METADATA +179 -0
- pdf_tool_cli-1.1.0.dist-info/RECORD +66 -0
- pdf_tool_cli-1.1.0.dist-info/WHEEL +4 -0
- pdf_tool_cli-1.1.0.dist-info/entry_points.txt +4 -0
- pdf_tool_cli-1.1.0.dist-info/licenses/LICENSE +21 -0
- pdftool/__init__.py +3 -0
- pdftool/cli/__init__.py +179 -0
- pdftool/cli/__main__.py +5 -0
- pdftool/cli/args.py +138 -0
- pdftool/cli/report.py +71 -0
- pdftool/core/__init__.py +0 -0
- pdftool/core/atomic.py +43 -0
- pdftool/core/batch.py +111 -0
- pdftool/core/config.py +89 -0
- pdftool/core/errors.py +36 -0
- pdftool/core/jobs.py +188 -0
- pdftool/core/logger.py +84 -0
- pdftool/core/naming.py +91 -0
- pdftool/core/plugin.py +96 -0
- pdftool/core/registry.py +59 -0
- pdftool/core/thumbnails.py +37 -0
- pdftool/core/updater.py +67 -0
- pdftool/main.py +30 -0
- pdftool/tools/__init__.py +0 -0
- pdftool/tools/compress/__init__.py +0 -0
- pdftool/tools/compress/logic.py +387 -0
- pdftool/tools/compress/panel.py +72 -0
- pdftool/tools/compress/params.py +18 -0
- pdftool/tools/compress/spec.py +18 -0
- pdftool/tools/images2pdf/__init__.py +0 -0
- pdftool/tools/images2pdf/logic.py +127 -0
- pdftool/tools/images2pdf/panel.py +22 -0
- pdftool/tools/images2pdf/params.py +6 -0
- pdftool/tools/images2pdf/spec.py +19 -0
- pdftool/tools/merge/__init__.py +0 -0
- pdftool/tools/merge/logic.py +54 -0
- pdftool/tools/merge/panel.py +44 -0
- pdftool/tools/merge/params.py +20 -0
- pdftool/tools/merge/spec.py +19 -0
- pdftool/tools/protect/__init__.py +0 -0
- pdftool/tools/protect/logic.py +85 -0
- pdftool/tools/protect/panel.py +72 -0
- pdftool/tools/protect/params.py +18 -0
- pdftool/tools/protect/spec.py +18 -0
- pdftool/tools/split/__init__.py +0 -0
- pdftool/tools/split/logic.py +139 -0
- pdftool/tools/split/panel.py +129 -0
- pdftool/tools/split/params.py +28 -0
- pdftool/tools/split/spec.py +19 -0
- pdftool/tools/watermark/__init__.py +0 -0
- pdftool/tools/watermark/logic.py +110 -0
- pdftool/tools/watermark/panel.py +67 -0
- pdftool/tools/watermark/params.py +19 -0
- pdftool/tools/watermark/spec.py +19 -0
- pdftool/ui/__init__.py +0 -0
- pdftool/ui/accessibility.py +44 -0
- pdftool/ui/app.py +425 -0
- pdftool/ui/errors.py +12 -0
- pdftool/ui/file_list.py +275 -0
- pdftool/ui/logs.py +58 -0
- pdftool/ui/output_dir.py +127 -0
- pdftool/ui/panel_base.py +631 -0
- pdftool/ui/platform.py +30 -0
- pdftool/ui/registry.py +61 -0
- pdftool/ui/theme.py +30 -0
- pdftool/ui/thumbnails.py +148 -0
pdftool/ui/app.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
import flet as ft
|
|
7
|
+
|
|
8
|
+
from pdftool import __version__
|
|
9
|
+
from pdftool.core.config import load_settings, save_settings
|
|
10
|
+
from pdftool.core.jobs import run_job, shutdown_job_executor
|
|
11
|
+
from pdftool.core.plugin import ToolContext
|
|
12
|
+
from pdftool.core.updater import GITHUB_REPO, check_for_update
|
|
13
|
+
from pdftool.ui import registry
|
|
14
|
+
from pdftool.ui.accessibility import icon_content
|
|
15
|
+
from pdftool.ui.logs import LOG_LABEL, download_log_button, make_log_picker
|
|
16
|
+
from pdftool.ui.theme import build_theme, next_mode, resolve_mode
|
|
17
|
+
from pdftool.ui.thumbnails import shutdown_thumbnail_executor
|
|
18
|
+
|
|
19
|
+
AUTHOR_HANDLE = "@juanmaav92"
|
|
20
|
+
AUTHOR_SITE = "https://juanMaAV92.github.io"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_footer(theme_label, on_theme, on_log, on_author):
|
|
24
|
+
"""Pie: firma del autor a la izquierda; versión y utilidades a la derecha.
|
|
25
|
+
|
|
26
|
+
Devuelve (pie, ítem del tema): el ítem se reetiqueta al cambiar el tema.
|
|
27
|
+
"""
|
|
28
|
+
theme_item = ft.PopupMenuItem(
|
|
29
|
+
text=theme_label, icon=ft.Icons.BRIGHTNESS_6, on_click=on_theme
|
|
30
|
+
)
|
|
31
|
+
# Visible en todas las pantallas pero discreta: mismo tono apagado y
|
|
32
|
+
# tamaño que la versión, sin competir con el resultado del panel.
|
|
33
|
+
credit = ft.TextButton(
|
|
34
|
+
f"Hecho por {AUTHOR_HANDLE}",
|
|
35
|
+
icon=ft.Icons.OPEN_IN_NEW,
|
|
36
|
+
tooltip="Visitar el sitio del autor",
|
|
37
|
+
on_click=on_author,
|
|
38
|
+
style=ft.ButtonStyle(
|
|
39
|
+
color=ft.Colors.ON_SURFACE_VARIANT,
|
|
40
|
+
icon_size=14,
|
|
41
|
+
text_style=ft.TextStyle(size=12),
|
|
42
|
+
padding=ft.padding.symmetric(horizontal=8, vertical=4),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
footer = ft.Row(
|
|
46
|
+
[
|
|
47
|
+
credit,
|
|
48
|
+
ft.Container(expand=True),
|
|
49
|
+
ft.Text(
|
|
50
|
+
f"v{__version__}",
|
|
51
|
+
size=11,
|
|
52
|
+
color=ft.Colors.ON_SURFACE_VARIANT,
|
|
53
|
+
tooltip=f"Versión de la aplicación: {__version__}",
|
|
54
|
+
),
|
|
55
|
+
ft.PopupMenuButton(
|
|
56
|
+
content=icon_content(ft.Icons.MORE_HORIZ, "Opciones de la aplicación"),
|
|
57
|
+
tooltip="Opciones de la aplicación",
|
|
58
|
+
items=[
|
|
59
|
+
theme_item,
|
|
60
|
+
ft.PopupMenuItem(
|
|
61
|
+
text=LOG_LABEL, icon=ft.Icons.DOWNLOAD, on_click=on_log
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
return footer, theme_item
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _tool_card(index, tool, on_open):
|
|
71
|
+
return ft.OutlinedButton(
|
|
72
|
+
content=ft.Column(
|
|
73
|
+
[
|
|
74
|
+
# La categoría va en la esquina, en la misma línea que el icono:
|
|
75
|
+
# agrupa sin añadir altura (la barra lateral ya muestra grupos).
|
|
76
|
+
ft.Row(
|
|
77
|
+
[
|
|
78
|
+
ft.Icon(tool.icon, size=30),
|
|
79
|
+
ft.Text(
|
|
80
|
+
tool.meta.category,
|
|
81
|
+
size=12,
|
|
82
|
+
color=ft.Colors.ON_SURFACE_VARIANT,
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
|
86
|
+
vertical_alignment=ft.CrossAxisAlignment.START,
|
|
87
|
+
),
|
|
88
|
+
ft.Text(tool.meta.name, weight=ft.FontWeight.BOLD, size=15),
|
|
89
|
+
ft.Text(
|
|
90
|
+
tool.meta.description, size=12, color=ft.Colors.ON_SURFACE_VARIANT
|
|
91
|
+
),
|
|
92
|
+
],
|
|
93
|
+
spacing=6,
|
|
94
|
+
tight=True,
|
|
95
|
+
width=208,
|
|
96
|
+
height=108,
|
|
97
|
+
horizontal_alignment=ft.CrossAxisAlignment.START,
|
|
98
|
+
),
|
|
99
|
+
width=240,
|
|
100
|
+
height=140,
|
|
101
|
+
style=ft.ButtonStyle(
|
|
102
|
+
padding=16,
|
|
103
|
+
alignment=ft.alignment.top_left,
|
|
104
|
+
color=ft.Colors.ON_SURFACE,
|
|
105
|
+
shape=ft.RoundedRectangleBorder(radius=14),
|
|
106
|
+
side={
|
|
107
|
+
ft.ControlState.DEFAULT: ft.BorderSide(1, ft.Colors.OUTLINE_VARIANT),
|
|
108
|
+
ft.ControlState.FOCUSED: ft.BorderSide(2, ft.Colors.PRIMARY),
|
|
109
|
+
},
|
|
110
|
+
bgcolor={ft.ControlState.FOCUSED: ft.Colors.PRIMARY_CONTAINER},
|
|
111
|
+
),
|
|
112
|
+
on_click=lambda _e: on_open(index),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _group_by_category(tools):
|
|
117
|
+
"""[(categoría, [(índice, herramienta), …]), …] respetando el orden de
|
|
118
|
+
`tools` (ya ordenadas por `meta.order`); cada categoría aparece donde lo
|
|
119
|
+
hace su primera herramienta."""
|
|
120
|
+
groups: dict[str, list] = {}
|
|
121
|
+
for index, tool in enumerate(tools):
|
|
122
|
+
groups.setdefault(tool.meta.category, []).append((index, tool))
|
|
123
|
+
return list(groups.items())
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _build_home(tools, on_open):
|
|
127
|
+
# Una sola cuadrícula que se ajusta al ancho: las herramientas ya llegan
|
|
128
|
+
# ordenadas por tarea, así que las de una misma categoría quedan juntas.
|
|
129
|
+
# (Una fila por categoría dejaba la pantalla medio vacía y forzaba scroll.)
|
|
130
|
+
cards = ft.Row(
|
|
131
|
+
[_tool_card(i, t, on_open) for i, t in enumerate(tools)],
|
|
132
|
+
wrap=True,
|
|
133
|
+
spacing=16,
|
|
134
|
+
run_spacing=16,
|
|
135
|
+
)
|
|
136
|
+
return ft.Column(
|
|
137
|
+
[
|
|
138
|
+
ft.Text("Herramientas PDF", size=28, weight=ft.FontWeight.BOLD),
|
|
139
|
+
ft.Text("Elige una herramienta para empezar."),
|
|
140
|
+
ft.Container(height=8),
|
|
141
|
+
cards,
|
|
142
|
+
],
|
|
143
|
+
spacing=14,
|
|
144
|
+
scroll=ft.ScrollMode.AUTO,
|
|
145
|
+
expand=True,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _nav_style(selected: bool) -> ft.ButtonStyle:
|
|
150
|
+
return ft.ButtonStyle(
|
|
151
|
+
bgcolor=ft.Colors.SECONDARY_CONTAINER if selected else None,
|
|
152
|
+
color=ft.Colors.ON_SECONDARY_CONTAINER if selected else ft.Colors.ON_SURFACE,
|
|
153
|
+
alignment=ft.alignment.center_left,
|
|
154
|
+
padding=ft.padding.symmetric(horizontal=12, vertical=10),
|
|
155
|
+
shape=ft.RoundedRectangleBorder(radius=10),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _nav_button(icon, label: str, on_click) -> ft.TextButton:
|
|
160
|
+
return ft.TextButton(
|
|
161
|
+
content=ft.Row(
|
|
162
|
+
[ft.Icon(icon, size=20), ft.Text(label, size=14)],
|
|
163
|
+
spacing=12,
|
|
164
|
+
),
|
|
165
|
+
tooltip=label,
|
|
166
|
+
style=_nav_style(False),
|
|
167
|
+
on_click=on_click,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _Sidebar(ft.Container):
|
|
172
|
+
"""Navegación lateral: Inicio y herramientas agrupadas por categoría.
|
|
173
|
+
|
|
174
|
+
Sustituye al NavigationRail, que no tiene scroll: con más herramientas las
|
|
175
|
+
entradas dejarían de caber en la ventana. Botones normales, así que se
|
|
176
|
+
recorre con el teclado como el resto de la app.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, tools, on_home, on_tool) -> None:
|
|
180
|
+
self.home_button = _nav_button(
|
|
181
|
+
ft.Icons.GRID_VIEW, "Inicio", lambda _e: on_home()
|
|
182
|
+
)
|
|
183
|
+
self.tool_buttons: list[ft.TextButton] = [
|
|
184
|
+
_nav_button(tool.icon, tool.meta.name, lambda _e, i=index: on_tool(i))
|
|
185
|
+
for index, tool in enumerate(tools)
|
|
186
|
+
]
|
|
187
|
+
controls: list[ft.Control] = [self.home_button]
|
|
188
|
+
for category, items in _group_by_category(tools):
|
|
189
|
+
controls.append(
|
|
190
|
+
ft.Container(
|
|
191
|
+
ft.Text(
|
|
192
|
+
category,
|
|
193
|
+
size=12,
|
|
194
|
+
weight=ft.FontWeight.W_600,
|
|
195
|
+
color=ft.Colors.ON_SURFACE_VARIANT,
|
|
196
|
+
),
|
|
197
|
+
padding=ft.padding.only(left=12, top=12, bottom=2),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
controls.extend(self.tool_buttons[i] for i, _tool in items)
|
|
201
|
+
self.list = ft.ListView(controls, spacing=2, padding=8, expand=True)
|
|
202
|
+
super().__init__(content=self.list, width=208)
|
|
203
|
+
|
|
204
|
+
def select(self, index: int | None) -> None:
|
|
205
|
+
"""Marca Inicio (None) o la herramienta `index` como actual."""
|
|
206
|
+
self.home_button.style = _nav_style(index is None)
|
|
207
|
+
for i, button in enumerate(self.tool_buttons):
|
|
208
|
+
button.style = _nav_style(i == index)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class _UpdateBanner(ft.Container):
|
|
212
|
+
"""Aviso de nueva versión como barra dentro de la ventana, encima del contenido.
|
|
213
|
+
|
|
214
|
+
No es un `ft.Banner`: abierto con `page.open` se dibujaba como capa encima,
|
|
215
|
+
oscurecido y tapando el título. Aquí empuja el contenido hacia abajo, y los
|
|
216
|
+
pares PRIMARY_CONTAINER / ON_PRIMARY_CONTAINER dan buen contraste en tema claro
|
|
217
|
+
y oscuro. Siempre se puede cerrar.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def __init__(self, page) -> None:
|
|
221
|
+
self._app_page = page
|
|
222
|
+
self._url: str | None = None
|
|
223
|
+
self.text = ft.Text(
|
|
224
|
+
"Hay una nueva versión disponible. ¿Descargarla ahora?",
|
|
225
|
+
color=ft.Colors.ON_PRIMARY_CONTAINER,
|
|
226
|
+
weight=ft.FontWeight.W_500,
|
|
227
|
+
expand=True,
|
|
228
|
+
)
|
|
229
|
+
self.download_button = ft.FilledButton("Descargar", on_click=self._download)
|
|
230
|
+
self.dismiss_button = ft.TextButton(
|
|
231
|
+
"Ahora no",
|
|
232
|
+
on_click=self._dismiss,
|
|
233
|
+
style=ft.ButtonStyle(color=ft.Colors.ON_PRIMARY_CONTAINER),
|
|
234
|
+
)
|
|
235
|
+
super().__init__(
|
|
236
|
+
content=ft.Row(
|
|
237
|
+
[
|
|
238
|
+
ft.Icon(
|
|
239
|
+
ft.Icons.SYSTEM_UPDATE, color=ft.Colors.ON_PRIMARY_CONTAINER
|
|
240
|
+
),
|
|
241
|
+
self.text,
|
|
242
|
+
self.dismiss_button,
|
|
243
|
+
self.download_button,
|
|
244
|
+
],
|
|
245
|
+
spacing=12,
|
|
246
|
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
|
247
|
+
),
|
|
248
|
+
bgcolor=ft.Colors.PRIMARY_CONTAINER,
|
|
249
|
+
padding=ft.padding.symmetric(horizontal=20, vertical=10),
|
|
250
|
+
visible=False,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def show(self, url: str) -> None:
|
|
254
|
+
self._url = url
|
|
255
|
+
self.visible = True
|
|
256
|
+
self._app_page.update()
|
|
257
|
+
|
|
258
|
+
def _download(self, _e) -> None:
|
|
259
|
+
if self._url:
|
|
260
|
+
self._app_page.launch_url(self._url)
|
|
261
|
+
self._dismiss(_e)
|
|
262
|
+
|
|
263
|
+
def _dismiss(self, _e) -> None:
|
|
264
|
+
self.visible = False
|
|
265
|
+
self._app_page.update()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _build_update_banner(page) -> _UpdateBanner:
|
|
269
|
+
return _UpdateBanner(page)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _start_update_check(
|
|
273
|
+
on_found: Callable[[str], None], check: Callable[[], str | None]
|
|
274
|
+
) -> threading.Thread:
|
|
275
|
+
"""Busca una versión nueva en un hilo propio.
|
|
276
|
+
|
|
277
|
+
No usa el pool de trabajos: con solo dos workers, una red lenta no debe
|
|
278
|
+
retrasar la primera tarea del usuario. `check` nunca lanza (ver updater).
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def target() -> None:
|
|
282
|
+
url = check()
|
|
283
|
+
if url:
|
|
284
|
+
on_found(url)
|
|
285
|
+
|
|
286
|
+
thread = threading.Thread(target=target, name="pdftool-update-check", daemon=True)
|
|
287
|
+
thread.start()
|
|
288
|
+
return thread
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
_MODE_NAMES = {"system": "Sistema", "light": "Claro", "dark": "Oscuro"}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _theme_label(mode: str) -> str:
|
|
295
|
+
return f"Tema: {_MODE_NAMES[mode]} · cambiar a {_MODE_NAMES[next_mode(mode)]}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _configure_window(page) -> None:
|
|
299
|
+
page.window.width = 980
|
|
300
|
+
page.window.height = 680
|
|
301
|
+
# Barra lateral (208) + contenido: por debajo de esto los controles de los
|
|
302
|
+
# paneles dejan de caber.
|
|
303
|
+
page.window.min_width = 820
|
|
304
|
+
page.window.min_height = 560
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _handle_shortcut(event, tool) -> bool:
|
|
308
|
+
"""⌘/Ctrl+O añade archivos y ⌘/Ctrl+Enter ejecuta en la herramienta abierta.
|
|
309
|
+
|
|
310
|
+
Devuelve True si el evento era un atajo (aunque no hubiera nada que hacer).
|
|
311
|
+
"""
|
|
312
|
+
if tool is None or not (event.ctrl or event.meta):
|
|
313
|
+
return False
|
|
314
|
+
if event.key.upper() == "O":
|
|
315
|
+
tool.shortcut_open()
|
|
316
|
+
return True
|
|
317
|
+
if event.key == "Enter":
|
|
318
|
+
tool.shortcut_run()
|
|
319
|
+
return True
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _install_shutdown_hooks(page, shutdown: Callable[[], None]) -> None:
|
|
324
|
+
"""Engancha `shutdown` al cierre y a la desconexión de la página.
|
|
325
|
+
|
|
326
|
+
Como corrutinas: Flet ejecuta los manejadores síncronos en su pool de
|
|
327
|
+
hilos, que al cerrar la ventana ya está apagado, y el cierre acababa con
|
|
328
|
+
«cannot schedule new futures after shutdown». `shutdown` no bloquea.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
async def on_shutdown(_e) -> None:
|
|
332
|
+
shutdown()
|
|
333
|
+
|
|
334
|
+
page.on_close = on_shutdown
|
|
335
|
+
page.on_disconnect = on_shutdown
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _notify(page, message: str) -> None:
|
|
339
|
+
page.open(ft.SnackBar(ft.Text(message)))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def build_app(page: ft.Page) -> None:
|
|
343
|
+
registry.discover()
|
|
344
|
+
tools = registry.get_tools()
|
|
345
|
+
settings = load_settings()
|
|
346
|
+
|
|
347
|
+
# Libera los workers compartidos cuando Flet cierra la sesión de escritorio
|
|
348
|
+
# o desconecta la vista web. Las tareas en curso ya tienen además su propio
|
|
349
|
+
# token de generación y dejan de notificar a la UI obsoleta.
|
|
350
|
+
def shutdown_resources() -> None:
|
|
351
|
+
shutdown_job_executor()
|
|
352
|
+
shutdown_thumbnail_executor()
|
|
353
|
+
|
|
354
|
+
_install_shutdown_hooks(page, shutdown_resources)
|
|
355
|
+
|
|
356
|
+
page.title = f"pdf-tool · v{__version__}"
|
|
357
|
+
page.theme = build_theme()
|
|
358
|
+
page.theme_mode = resolve_mode(settings.theme_mode)
|
|
359
|
+
_configure_window(page)
|
|
360
|
+
|
|
361
|
+
ctx = ToolContext(page=page, run_job=run_job, settings=settings)
|
|
362
|
+
content = ft.Container(expand=True, padding=24)
|
|
363
|
+
|
|
364
|
+
current: list = [None] # herramienta abierta (None = Inicio)
|
|
365
|
+
|
|
366
|
+
def open_tool(index: int) -> None:
|
|
367
|
+
content.content = tools[index].build_panel(ctx)
|
|
368
|
+
current[0] = tools[index]
|
|
369
|
+
sidebar.select(index)
|
|
370
|
+
page.update()
|
|
371
|
+
|
|
372
|
+
def open_home() -> None:
|
|
373
|
+
content.content = _build_home(tools, open_tool)
|
|
374
|
+
current[0] = None
|
|
375
|
+
sidebar.select(None)
|
|
376
|
+
page.update()
|
|
377
|
+
|
|
378
|
+
page.on_keyboard_event = lambda e: _handle_shortcut(e, current[0])
|
|
379
|
+
|
|
380
|
+
sidebar = _Sidebar(tools, on_home=open_home, on_tool=open_tool)
|
|
381
|
+
|
|
382
|
+
def toggle_theme(_e) -> None:
|
|
383
|
+
settings.theme_mode = next_mode(settings.theme_mode)
|
|
384
|
+
save_settings(settings)
|
|
385
|
+
page.theme_mode = resolve_mode(settings.theme_mode)
|
|
386
|
+
refresh_theme_label()
|
|
387
|
+
page.update()
|
|
388
|
+
|
|
389
|
+
def refresh_theme_label() -> None:
|
|
390
|
+
theme_item.text = _theme_label(settings.theme_mode)
|
|
391
|
+
|
|
392
|
+
update_banner = _build_update_banner(page)
|
|
393
|
+
|
|
394
|
+
log_picker = make_log_picker(
|
|
395
|
+
on_saved=lambda path: _notify(page, f"Informe guardado en «{path.name}».")
|
|
396
|
+
)
|
|
397
|
+
page.overlay.append(log_picker)
|
|
398
|
+
|
|
399
|
+
footer, theme_item = _build_footer(
|
|
400
|
+
"Cambiar tema",
|
|
401
|
+
toggle_theme,
|
|
402
|
+
download_log_button(log_picker).on_click,
|
|
403
|
+
lambda _e: page.launch_url(AUTHOR_SITE),
|
|
404
|
+
)
|
|
405
|
+
refresh_theme_label()
|
|
406
|
+
|
|
407
|
+
page.add(
|
|
408
|
+
ft.Column(
|
|
409
|
+
[
|
|
410
|
+
update_banner,
|
|
411
|
+
ft.Row([sidebar, ft.VerticalDivider(width=1), content], expand=True),
|
|
412
|
+
ft.Divider(height=1),
|
|
413
|
+
footer,
|
|
414
|
+
],
|
|
415
|
+
expand=True,
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
open_home()
|
|
419
|
+
|
|
420
|
+
def _on_update(url: str) -> None:
|
|
421
|
+
update_banner.show(url)
|
|
422
|
+
|
|
423
|
+
_start_update_check(
|
|
424
|
+
_on_update, lambda: check_for_update(current=__version__, repo=GITHUB_REPO)
|
|
425
|
+
)
|
pdftool/ui/errors.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pdftool.core.errors import user_message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def humanize_error(exc: Exception) -> tuple[str, str | None]:
|
|
7
|
+
"""Traduce una excepción a (mensaje para el usuario, detalle técnico | None).
|
|
8
|
+
|
|
9
|
+
La regla vive en `core.errors` para que la lógica por lotes la aplique
|
|
10
|
+
también a cada archivo; aquí solo se expone a la UI.
|
|
11
|
+
"""
|
|
12
|
+
return user_message(exc)
|