pyflutter-app 0.3.10__tar.gz → 0.3.11__tar.gz
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.
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/PKG-INFO +1 -1
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/cartas/__init__.py +2 -2
- pyflutter_app-0.3.10/converter/home_page/view.py → pyflutter_app-0.3.11/converter/home_page/page_sheets.py +9 -48
- pyflutter_app-0.3.11/converter/home_page/view.py +71 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/controller_forms.py +55 -3
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/model_forms.py +15 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/view_forms.py +11 -14
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_image/common.py +65 -6
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_image/controller_image.py +130 -4
- pyflutter_app-0.3.11/converter/page_image/model_image.py +222 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_image/view_image.py +46 -0
- pyflutter_app-0.3.11/converter/page_ocr/__init__.py +4 -0
- pyflutter_app-0.3.11/converter/page_ocr/common.py +55 -0
- pyflutter_app-0.3.11/converter/page_ocr/controller_ocr.py +338 -0
- pyflutter_app-0.3.11/converter/page_ocr/model_ocr.py +191 -0
- pyflutter_app-0.3.11/converter/page_ocr/view_ocr.py +183 -0
- pyflutter_app-0.3.11/converter/page_whatsapp/PATCH_whatsapp_files.md +245 -0
- pyflutter_app-0.3.11/converter/page_whatsapp/__init__.py +4 -0
- pyflutter_app-0.3.11/converter/page_whatsapp/common.py +41 -0
- pyflutter_app-0.3.11/converter/page_whatsapp/controller_whatsapp.py +370 -0
- pyflutter_app-0.3.11/converter/page_whatsapp/model_whatsapp.py +353 -0
- pyflutter_app-0.3.11/converter/page_whatsapp/view_whatsapp.py +262 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/run_app_convert.py +4 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/table_widget.py +34 -16
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyproject.toml +1 -1
- pyflutter_app-0.3.11/teste.py +9 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/uv.lock +1 -1
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/whatsapp_files/__init__.py +150 -49
- pyflutter_app-0.3.10/converter/page_image/model_image.py +0 -95
- pyflutter_app-0.3.10/converter/whatsapp/__init__.py +0 -0
- pyflutter_app-0.3.10/converter/whatsapp/model.py +0 -29
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.gitignore +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/.gitignore +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/inspectionProfiles/Project_Default.xml +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/misc.xml +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/modules.xml +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/pyflutter_app.iml +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/.idea/vcs.xml +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/README.md +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/cartas/origin.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/convert_stream/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/__main__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/assets/forms_config.json +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/config_app.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/home_page/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/home_page/_page_edit.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/home_page/controller.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/home_page/model.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/lib_toi.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_circuit/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_circuit/common.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_circuit/controller_circuit.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_circuit/model_circuit.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_circuit/service.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_circuit/view_circuit.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/Diagnorstico.txt +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/common.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/config.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/diagnose_forms.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/master_forms.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_forms/service.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_image/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_image/renderer.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/page_image/widgets.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/converter/util.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/local_build/linux.sh +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/local_build/send.sh +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/main.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/mvc/controller.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/mvc/model.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/mvc/view.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/new-export_app.sh +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/ocr_stream/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/app.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/file_picker.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/navigator.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/scheduler.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/theme.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/buttons.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/container.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/core.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/list_builder.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/progress.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/tables.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/pyflutter_ui/ui/widgets.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/table_stream/__init__.py +0 -0
- {pyflutter_app-0.3.10 → pyflutter_app-0.3.11}/update.sh +0 -0
|
@@ -51,7 +51,7 @@ __version__ = "0.2"
|
|
|
51
51
|
class ConfigTOI:
|
|
52
52
|
|
|
53
53
|
file_relatorio_toi: File
|
|
54
|
-
|
|
54
|
+
file_cartas_lancadas: File
|
|
55
55
|
file_table_toi: File
|
|
56
56
|
file_master_forms: File # NOVO
|
|
57
57
|
dir_base_toi: Directory
|
|
@@ -126,7 +126,7 @@ class BuilderConfigTOI(object):
|
|
|
126
126
|
dir_base_toi=self.dir_retorno_de_campo,
|
|
127
127
|
file_relatorio_toi=self.file_relatorio_toi,
|
|
128
128
|
file_table_toi=self.file_table_toi,
|
|
129
|
-
|
|
129
|
+
file_cartas_lancadas=self.file_relatorio_forms,
|
|
130
130
|
dir_log_excel=self.dir_log_excel,
|
|
131
131
|
dir_relatorios=self.dir_relatorios,
|
|
132
132
|
file_master_forms=self.file_master_forms,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from pyflutter_ui import (
|
|
2
|
-
ChangeNotifier,
|
|
3
2
|
BuildContext,
|
|
4
3
|
FlutterWidget,
|
|
5
|
-
LinearProgress,
|
|
6
4
|
StatelessWidget,
|
|
7
5
|
Consumer,
|
|
8
6
|
Column,
|
|
@@ -23,10 +21,7 @@ from pyflutter_ui import (
|
|
|
23
21
|
)
|
|
24
22
|
|
|
25
23
|
from tkinter import messagebox
|
|
26
|
-
from typing import Any
|
|
27
24
|
import pandas as pd
|
|
28
|
-
from pandas import DataFrame
|
|
29
|
-
|
|
30
25
|
|
|
31
26
|
from table_stream import (
|
|
32
27
|
ArrayList,
|
|
@@ -40,25 +35,20 @@ from table_stream import (
|
|
|
40
35
|
from converter.home_page.controller import ControllerHomePage
|
|
41
36
|
from converter.table_widget import TableOrProgress
|
|
42
37
|
from converter.home_page._page_edit import PageEditExcel
|
|
43
|
-
|
|
44
|
-
# Páginas para navegação.
|
|
45
|
-
from converter.page_forms import PageRespondsForms
|
|
46
|
-
from converter.page_forms import ControllerForms
|
|
47
|
-
from converter.config_app import MapConfig, AppConfigNotifier
|
|
48
|
-
from converter.page_image import PageImage
|
|
49
|
-
from converter.page_circuit import PageCircuit
|
|
38
|
+
from converter.config_app import AppConfigNotifier
|
|
50
39
|
|
|
51
40
|
|
|
52
|
-
class
|
|
41
|
+
class PageSheets(StatelessWidget):
|
|
42
|
+
"""Importar/carregar planilhas. De lá se acessa a edição de dados."""
|
|
53
43
|
|
|
54
44
|
def __init__(self, key: str | None = None) -> None:
|
|
55
45
|
super().__init__(key)
|
|
56
46
|
|
|
57
47
|
def build(self, context: BuildContext) -> FlutterWidget:
|
|
58
|
-
return
|
|
48
|
+
return _PageSheetsState()
|
|
59
49
|
|
|
60
50
|
|
|
61
|
-
class
|
|
51
|
+
class _PageSheetsState(StatelessWidget):
|
|
62
52
|
|
|
63
53
|
def __init__(self, key: str | None = None) -> None:
|
|
64
54
|
super().__init__(key)
|
|
@@ -95,8 +85,7 @@ class _HomePageState(StatelessWidget):
|
|
|
95
85
|
|
|
96
86
|
@property
|
|
97
87
|
def app_config(self) -> AppConfigNotifier:
|
|
98
|
-
|
|
99
|
-
return state
|
|
88
|
+
return Provider.read(AppConfigNotifier)
|
|
100
89
|
|
|
101
90
|
@property
|
|
102
91
|
def controller(self) -> ControllerHomePage:
|
|
@@ -232,29 +221,11 @@ class _HomePageState(StatelessWidget):
|
|
|
232
221
|
df=data.head(),
|
|
233
222
|
)
|
|
234
223
|
|
|
235
|
-
def action_open_forms(self, context: BuildContext, controller: ControllerHomePage):
|
|
236
|
-
"""
|
|
237
|
-
Seta o DataFrame() da página atual como propriedade do ControllerForms()
|
|
238
|
-
antes de carregar a página.
|
|
239
|
-
"""
|
|
240
|
-
# df = controller.data_notifier.value
|
|
241
|
-
# forms_controller: ControllerForms = context.read(ControllerForms)
|
|
242
|
-
# forms_controller.set_data(df)
|
|
243
|
-
# if not forms_controller.forms_state.data_is_valid():
|
|
244
|
-
# messagebox.showerror("Erro", "Tabela TOI inválida")
|
|
245
|
-
# return
|
|
246
|
-
|
|
247
|
-
context.navigator.push(PageRespondsForms())
|
|
248
|
-
|
|
249
224
|
def _builder_body(
|
|
250
225
|
self, context: BuildContext, controller: ControllerHomePage
|
|
251
226
|
) -> FlutterWidget:
|
|
252
227
|
self.context = context
|
|
253
228
|
|
|
254
|
-
filename = controller.get_file_sheet()
|
|
255
|
-
if filename is not None:
|
|
256
|
-
filename = filename.basename()
|
|
257
|
-
|
|
258
229
|
return Column(
|
|
259
230
|
children=[
|
|
260
231
|
# -----------------------------------#
|
|
@@ -315,24 +286,14 @@ class _HomePageState(StatelessWidget):
|
|
|
315
286
|
Expanded(Consumer(ControllerHomePage, self._build_table)),
|
|
316
287
|
Row(
|
|
317
288
|
[
|
|
289
|
+
ElevatedButton(
|
|
290
|
+
"<- Voltar", lambda: context.navigator.pop()
|
|
291
|
+
),
|
|
318
292
|
Text("Ir para: "),
|
|
319
293
|
ElevatedButton(
|
|
320
294
|
"Edição de dados",
|
|
321
295
|
lambda: context.navigator.push(PageEditExcel()),
|
|
322
296
|
),
|
|
323
|
-
# ElevatedButton("Exportar dados", lambda: print()),
|
|
324
|
-
ElevatedButton(
|
|
325
|
-
"Responds Forms",
|
|
326
|
-
lambda: self.action_open_forms(context, controller),
|
|
327
|
-
),
|
|
328
|
-
ElevatedButton(
|
|
329
|
-
"Renomear imagens",
|
|
330
|
-
lambda: context.navigator.push(PageImage()),
|
|
331
|
-
),
|
|
332
|
-
ElevatedButton(
|
|
333
|
-
"Dividir base (QGIS)",
|
|
334
|
-
lambda: context.navigator.push(PageCircuit()),
|
|
335
|
-
),
|
|
336
297
|
],
|
|
337
298
|
main_axis_alignment=MainAxisAlignment.START,
|
|
338
299
|
),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from pyflutter_ui import (
|
|
4
|
+
BuildContext,
|
|
5
|
+
FlutterWidget,
|
|
6
|
+
StatelessWidget,
|
|
7
|
+
Column,
|
|
8
|
+
Row,
|
|
9
|
+
ElevatedButton,
|
|
10
|
+
Divider,
|
|
11
|
+
MainAxisAlignment,
|
|
12
|
+
Scaffold,
|
|
13
|
+
Text,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Páginas para navegação.
|
|
17
|
+
from converter.home_page.page_sheets import PageSheets
|
|
18
|
+
from converter.page_forms import PageRespondsForms
|
|
19
|
+
from converter.page_image import PageImage
|
|
20
|
+
from converter.page_circuit import PageCircuit
|
|
21
|
+
from converter.page_whatsapp import PageWhatsApp
|
|
22
|
+
from converter.page_ocr import PageOcr
|
|
23
|
+
from pyflutter_ui.ui.core import GlobalKey
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HomePage(StatelessWidget):
|
|
27
|
+
"""Home: somente botões para navegar entre as páginas."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, key: GlobalKey | None = None) -> None:
|
|
30
|
+
super().__init__(key)
|
|
31
|
+
|
|
32
|
+
def build(self, context: BuildContext) -> FlutterWidget:
|
|
33
|
+
return _HomePageState()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _HomePageState(StatelessWidget):
|
|
37
|
+
|
|
38
|
+
# (rótulo do botão, descrição, página)
|
|
39
|
+
PAGES: list[tuple[str, str, Callable[[], FlutterWidget]]] = [
|
|
40
|
+
("Planilhas", "Importar/carregar planilhas e editar dados", PageSheets),
|
|
41
|
+
("WhatsApp", "Importar chats (.zip) para o cache do WhatsApp", PageWhatsApp),
|
|
42
|
+
("Responds Forms", "Lançar cartas TOI no Microsoft Forms", PageRespondsForms),
|
|
43
|
+
("Renomear imagens", "Identificar cartas (UC / TOI / data)", PageImage),
|
|
44
|
+
("Dividir base (QGIS)", "Dividir a base por coluna (CSV/Excel)", PageCircuit),
|
|
45
|
+
("OCR", "Aplicar OCR em PDFs e imagens de uma pasta", PageOcr),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def __init__(self, key: GlobalKey | None = None) -> None:
|
|
49
|
+
super().__init__(key)
|
|
50
|
+
|
|
51
|
+
def _nav_item(
|
|
52
|
+
self,
|
|
53
|
+
context: BuildContext,
|
|
54
|
+
label: str,
|
|
55
|
+
description: str,
|
|
56
|
+
page: Callable[[], FlutterWidget],
|
|
57
|
+
) -> FlutterWidget:
|
|
58
|
+
return Row(
|
|
59
|
+
[
|
|
60
|
+
ElevatedButton(label, lambda: context.navigator.push(page())),
|
|
61
|
+
Text(description),
|
|
62
|
+
],
|
|
63
|
+
main_axis_alignment=MainAxisAlignment.START,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def build(self, context: BuildContext) -> FlutterWidget:
|
|
67
|
+
items = [
|
|
68
|
+
self._nav_item(context, label, desc, page)
|
|
69
|
+
for label, desc, page in self.PAGES
|
|
70
|
+
]
|
|
71
|
+
return Scaffold(Column([Text("App Convert"), Divider(), *items]))
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
import os
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Callable
|
|
5
|
+
import threading
|
|
5
6
|
|
|
6
7
|
import pandas as pd
|
|
7
8
|
|
|
@@ -47,6 +48,8 @@ class ControllerForms(ChangeNotifier):
|
|
|
47
48
|
self.master = ControllerMasterForms()
|
|
48
49
|
self.generating: bool = False
|
|
49
50
|
|
|
51
|
+
self.scanning: bool = False # NOVO: lendo a pasta
|
|
52
|
+
|
|
50
53
|
# Log em Excel gravado durante o lançamento (1 arquivo por execução).
|
|
51
54
|
self.save_every: int = 2 # grava a cada N linhas concluídas (sucesso OU falha)
|
|
52
55
|
self.result_file: File | None = None
|
|
@@ -123,9 +126,10 @@ class ControllerForms(ChangeNotifier):
|
|
|
123
126
|
|
|
124
127
|
# ------------------------------------------------------------------ gerar tabela (cache)
|
|
125
128
|
def generate_table(self, files: ArrayList[File]) -> None:
|
|
126
|
-
if self.running or self.generating:
|
|
129
|
+
if self.running or self.generating or self.scanning:
|
|
127
130
|
self.show_info("Existe outra operação em andamento.")
|
|
128
131
|
return
|
|
132
|
+
|
|
129
133
|
if files.empty():
|
|
130
134
|
return
|
|
131
135
|
|
|
@@ -146,6 +150,54 @@ class ControllerForms(ChangeNotifier):
|
|
|
146
150
|
self.generating = False
|
|
147
151
|
self.show_error(str(e))
|
|
148
152
|
|
|
153
|
+
def generate_table_from_folder(self, folder: Directory) -> None:
|
|
154
|
+
"""Varre a pasta (recursivo, PDF/imagem) e gera a tabela só com o cache."""
|
|
155
|
+
if self.running or self.generating or self.scanning:
|
|
156
|
+
self.show_info("Existe outra operação em andamento.")
|
|
157
|
+
return
|
|
158
|
+
if not folder.exists():
|
|
159
|
+
self.show_error(f"A pasta não existe:\n{folder.absolute()}")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
self.scanning = True
|
|
163
|
+
self.status_text = f"Lendo {folder.absolute()} (incluindo subpastas) ..."
|
|
164
|
+
self.notify_listeners()
|
|
165
|
+
|
|
166
|
+
def work() -> None:
|
|
167
|
+
files: ArrayList[File] | None = None
|
|
168
|
+
err: Exception | None = None
|
|
169
|
+
try:
|
|
170
|
+
files = self.model.list_documents(folder)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
err = e
|
|
173
|
+
_ui(lambda: self._on_folder_scanned(folder, files, err))
|
|
174
|
+
|
|
175
|
+
threading.Thread(target=work, daemon=True, name="forms-scan").start()
|
|
176
|
+
|
|
177
|
+
def _on_folder_scanned(
|
|
178
|
+
self,
|
|
179
|
+
folder: Directory,
|
|
180
|
+
files: ArrayList[File] | None,
|
|
181
|
+
err: Exception | None,
|
|
182
|
+
) -> None:
|
|
183
|
+
self.scanning = False
|
|
184
|
+
|
|
185
|
+
if err is not None or files is None:
|
|
186
|
+
self.status_text = f"Erro ao ler a pasta: {err}"
|
|
187
|
+
self.notify_listeners()
|
|
188
|
+
self.show_error(f"Não foi possível ler a pasta:\n{err}")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if files.empty():
|
|
192
|
+
self.status_text = "Nenhum PDF/imagem encontrado na pasta."
|
|
193
|
+
self.notify_listeners()
|
|
194
|
+
self.show_info(f"Nenhum PDF ou imagem encontrado em:\n{folder.absolute()}")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self.status_text = f"{files.size()} arquivo(s) encontrados. Gerando tabela ..."
|
|
198
|
+
self.notify_listeners()
|
|
199
|
+
self.generate_table(files) # fluxo existente (hash + cache + Excel)
|
|
200
|
+
|
|
149
201
|
def _on_gen_progress(self, done: int, total: int) -> None:
|
|
150
202
|
self.progress = (done, total)
|
|
151
203
|
self.status_text = f"Calculando hashes {done}/{total}"
|
|
@@ -235,10 +287,10 @@ class ControllerForms(ChangeNotifier):
|
|
|
235
287
|
|
|
236
288
|
# ------------------------------------------------------------------ execução
|
|
237
289
|
def start_forms(self) -> None:
|
|
238
|
-
|
|
239
|
-
if self.running or self.generating:
|
|
290
|
+
if self.running or self.generating or self.scanning:
|
|
240
291
|
self.show_info("Já existe uma operação em andamento.")
|
|
241
292
|
return
|
|
293
|
+
|
|
242
294
|
if self.map_rows.is_empty():
|
|
243
295
|
self.show_error("Importe uma tabela TOI antes de iniciar.")
|
|
244
296
|
return
|
|
@@ -15,6 +15,8 @@ from table_stream import (
|
|
|
15
15
|
HashMapDict,
|
|
16
16
|
RowH,
|
|
17
17
|
TableData,
|
|
18
|
+
ContentFiles,
|
|
19
|
+
EnumDocuments,
|
|
18
20
|
load_file_hash,
|
|
19
21
|
)
|
|
20
22
|
from cartas import (
|
|
@@ -250,6 +252,19 @@ class ModelForms(object):
|
|
|
250
252
|
t = getattr(self, "_gen_thread", None)
|
|
251
253
|
return t is not None and t.is_alive()
|
|
252
254
|
|
|
255
|
+
@staticmethod
|
|
256
|
+
def list_documents(folder: Directory, max_files: int = 200_000) -> ArrayList[File]:
|
|
257
|
+
"""PDFs e imagens da pasta, lendo as subpastas (extensão sem diferenciar maiúsculas)."""
|
|
258
|
+
content = ContentFiles(folder, max_files=max_files)
|
|
259
|
+
everything = content.get_files(EnumDocuments.ALL, sort=True, recursive=True)
|
|
260
|
+
|
|
261
|
+
valid = ArrayList[File]()
|
|
262
|
+
for f in everything:
|
|
263
|
+
ext = (f.extension() or "").lower()
|
|
264
|
+
if ext == ".pdf" or ext in (".png", ".jpg", ".jpeg"):
|
|
265
|
+
valid.add(f)
|
|
266
|
+
return valid
|
|
267
|
+
|
|
253
268
|
def generate_table(
|
|
254
269
|
self,
|
|
255
270
|
files: ArrayList[File],
|
|
@@ -43,11 +43,8 @@ class _PageRespondsFormsState(StatelessWidget):
|
|
|
43
43
|
def __init__(self, key=None):
|
|
44
44
|
super().__init__(key)
|
|
45
45
|
|
|
46
|
-
self.
|
|
47
|
-
|
|
48
|
-
allow_multiple=True,
|
|
49
|
-
allowed_extensions=["pdf", "png", "jpg", "jpeg"],
|
|
50
|
-
dialog_title="Escolha as cartas (PDF/imagem)",
|
|
46
|
+
self.picker_cartas_dir = FilePickerController(
|
|
47
|
+
dialog_title="Escolha a pasta com as cartas (PDF/imagem)",
|
|
51
48
|
)
|
|
52
49
|
|
|
53
50
|
self.file_picker = FilePickerController(
|
|
@@ -70,21 +67,21 @@ class _PageRespondsFormsState(StatelessWidget):
|
|
|
70
67
|
|
|
71
68
|
# ------------------------------------------------------------------ ações
|
|
72
69
|
def action_generate_table(self, controller: ControllerForms) -> None:
|
|
73
|
-
if controller.running or controller.generating:
|
|
70
|
+
if controller.running or controller.generating or controller.scanning:
|
|
74
71
|
self.show_info("Existe outra operação em andamento.")
|
|
75
72
|
return
|
|
76
73
|
|
|
77
74
|
conf = controller.get_config()
|
|
78
|
-
|
|
75
|
+
if conf.value.initial_dir.exists():
|
|
76
|
+
self.picker_cartas_dir.initial_directory = conf.value.initial_dir.absolute()
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
if
|
|
78
|
+
path = self.picker_cartas_dir.pick_directory()
|
|
79
|
+
if not path:
|
|
82
80
|
return
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
controller.generate_table(ArrayList([File(p) for p in result.paths]))
|
|
82
|
+
folder = Directory(path)
|
|
83
|
+
conf.set_initial_dir(folder)
|
|
84
|
+
controller.generate_table_from_folder(folder)
|
|
88
85
|
|
|
89
86
|
def action_pick_table(self, controller: ControllerForms) -> None:
|
|
90
87
|
if controller.running:
|
|
@@ -122,7 +119,7 @@ class _PageRespondsFormsState(StatelessWidget):
|
|
|
122
119
|
Row(
|
|
123
120
|
[
|
|
124
121
|
ElevatedButton(
|
|
125
|
-
"Gerar tabela",
|
|
122
|
+
"Gerar tabela (pasta)",
|
|
126
123
|
lambda: self.action_generate_table(controller),
|
|
127
124
|
),
|
|
128
125
|
ElevatedButton(
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import re
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from enum import StrEnum
|
|
5
5
|
from typing import Final
|
|
6
6
|
|
|
7
7
|
from table_stream import File
|
|
8
|
-
from cartas import
|
|
8
|
+
from cartas import (
|
|
9
|
+
normalizar_data,
|
|
10
|
+
toi_valido,
|
|
11
|
+
uc_valida,
|
|
12
|
+
normalizar_data,
|
|
13
|
+
identificar_por_nome,
|
|
14
|
+
ResultadoUcToi,
|
|
15
|
+
OrigemUcToi,
|
|
16
|
+
)
|
|
9
17
|
|
|
10
18
|
# Extensões que o OpenCV consegue abrir (minúsculas). .svg fica de fora.
|
|
11
19
|
IMAGE_EXTENSIONS: Final[frozenset[str]] = frozenset(
|
|
@@ -35,8 +43,7 @@ class FieldError(ValueError):
|
|
|
35
43
|
def natural_key(f: File) -> list[str | int]:
|
|
36
44
|
"""Ordenação natural: img2.jpg vem antes de img10.jpg."""
|
|
37
45
|
return [
|
|
38
|
-
int(t) if t.isdigit() else t.lower()
|
|
39
|
-
for t in _RE_NATURAL.split(f.absolute())
|
|
46
|
+
int(t) if t.isdigit() else t.lower() for t in _RE_NATURAL.split(f.absolute())
|
|
40
47
|
]
|
|
41
48
|
|
|
42
49
|
|
|
@@ -82,11 +89,15 @@ class RenameRequest:
|
|
|
82
89
|
if not uc:
|
|
83
90
|
raise FieldError(FieldName.UC, "Informe a UC (obrigatória).")
|
|
84
91
|
if not _RE_UC.fullmatch(uc):
|
|
85
|
-
raise FieldError(
|
|
92
|
+
raise FieldError(
|
|
93
|
+
FieldName.UC, "A UC deve ter apenas números (4 a 10 dígitos)."
|
|
94
|
+
)
|
|
86
95
|
if not toi:
|
|
87
96
|
raise FieldError(FieldName.TOI, "Informe o TOI (obrigatório).")
|
|
88
97
|
if not _RE_TOI.fullmatch(toi):
|
|
89
|
-
raise FieldError(
|
|
98
|
+
raise FieldError(
|
|
99
|
+
FieldName.TOI, "O TOI deve ter apenas números (4 a 12 dígitos)."
|
|
100
|
+
)
|
|
90
101
|
|
|
91
102
|
date: tuple[str, str, str] | None = None
|
|
92
103
|
if date_text:
|
|
@@ -128,3 +139,51 @@ class ResultRename:
|
|
|
128
139
|
cached: bool
|
|
129
140
|
warnings: tuple[str, ...] = ()
|
|
130
141
|
cache_error: Exception | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Extensões aceitas no REGISTRO em cache (o visualizador só abre imagens).
|
|
145
|
+
REGISTER_EXTENSIONS: Final[frozenset[str]] = IMAGE_EXTENSIONS | {".pdf"}
|
|
146
|
+
|
|
147
|
+
_RE_NAME_UC_TOI: Final = re.compile(r"(\d+)[\s_]+(\d+)")
|
|
148
|
+
_RE_COLLISION: Final = re.compile(r"-\d{1,2}$")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def parse_named_file(f: File) -> ResultadoUcToi | None:
|
|
152
|
+
"""
|
|
153
|
+
Reconhece cartas já nomeadas:
|
|
154
|
+
'1305934 193166923.pdf' -> UC TOI (padrão novo)
|
|
155
|
+
'2026 11 03 1305934 193166923' -> data + UC + TOI
|
|
156
|
+
'UC 1305934 TOI 193166923' -> com rótulos
|
|
157
|
+
Sufixos de colisão '-1', '-2' são ignorados. None = fora do padrão.
|
|
158
|
+
"""
|
|
159
|
+
stem = _RE_COLLISION.sub("", f.name().strip()).strip()
|
|
160
|
+
|
|
161
|
+
m = _RE_NAME_UC_TOI.fullmatch(stem)
|
|
162
|
+
if m:
|
|
163
|
+
uc, toi = m.groups()
|
|
164
|
+
# UC 6-8 dígitos e TOI 8-10 (evita confundir "2026 11" com UC/TOI).
|
|
165
|
+
if uc_valida(uc) and toi_valido(toi):
|
|
166
|
+
return ResultadoUcToi(uc=uc, toi=toi, origem=OrigemUcToi.NOME_ARQUIVO)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
return identificar_por_nome(f.basename())
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class ResultRegisterNamed:
|
|
174
|
+
"""Resultado do registro em cache de cartas já nomeadas."""
|
|
175
|
+
|
|
176
|
+
total: int = 0 # arquivos verificados (imagem/pdf)
|
|
177
|
+
recognized: int = 0 # hashes únicas reconhecidas pelo nome
|
|
178
|
+
changed: int = 0 # linhas gravadas/atualizadas no cache TOI
|
|
179
|
+
with_date: int = 0 # reconhecidas que têm data (chat ou nome)
|
|
180
|
+
ignored: list[str] = field(default_factory=list)
|
|
181
|
+
failures: list[tuple[str, Exception]] = field(default_factory=list)
|
|
182
|
+
warnings: list[str] = field(default_factory=list)
|
|
183
|
+
|
|
184
|
+
moved: int = 0 # NOVO: arquivos movidos p/ Retorno-de-campo
|
|
185
|
+
moves: dict[str, str] = field(default_factory=dict) # NOVO: caminho antigo -> novo
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def without_date(self) -> int:
|
|
189
|
+
return self.recognized - self.with_date
|
|
@@ -15,6 +15,24 @@ from converter.page_image.common import (
|
|
|
15
15
|
)
|
|
16
16
|
from converter.page_image.model_image import ModelImage
|
|
17
17
|
from converter.page_image.widgets import TextEditingController
|
|
18
|
+
import threading
|
|
19
|
+
from pyflutter_ui.scheduler import MainThreadScheduler
|
|
20
|
+
from converter.page_image.common import (
|
|
21
|
+
FieldError,
|
|
22
|
+
FieldName,
|
|
23
|
+
RenameRequest,
|
|
24
|
+
ResultRename,
|
|
25
|
+
ResultRegisterNamed,
|
|
26
|
+
natural_key,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ui(fn: Callable[[], None]) -> None:
|
|
31
|
+
"""Executa fn na thread da UI (o registro roda em worker thread)."""
|
|
32
|
+
try:
|
|
33
|
+
MainThreadScheduler.run(fn)
|
|
34
|
+
except RuntimeError:
|
|
35
|
+
fn()
|
|
18
36
|
|
|
19
37
|
|
|
20
38
|
class ControllerImage(ChangeNotifier):
|
|
@@ -51,6 +69,9 @@ class ControllerImage(ChangeNotifier):
|
|
|
51
69
|
self.fn_show_error: Callable[[str], None] | None = None
|
|
52
70
|
self.fn_show_info: Callable[[str], None] | None = None
|
|
53
71
|
|
|
72
|
+
self.auto_register: bool = True # registrar no cache ao importar
|
|
73
|
+
self._reg_thread: threading.Thread | None = None
|
|
74
|
+
|
|
54
75
|
# ------------------------------------------------------------------ infra
|
|
55
76
|
def get_config(self) -> AppConfigNotifier:
|
|
56
77
|
return Provider.read(AppConfigNotifier)
|
|
@@ -101,11 +122,17 @@ class ControllerImage(ChangeNotifier):
|
|
|
101
122
|
self.show_error(f"A pasta não existe:\n{folder.absolute()}")
|
|
102
123
|
return
|
|
103
124
|
self.set_status(f"Lendo {folder.absolute()} ...")
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
images, registrable = self.model.scan_folder(folder)
|
|
126
|
+
origin = f"Pasta {folder.basename()}"
|
|
127
|
+
self._add(images, origin)
|
|
128
|
+
if self.auto_register:
|
|
129
|
+
self.register_named_files(registrable, origin) # inclui PDFs
|
|
106
130
|
|
|
107
131
|
def import_files(self, files: Iterable[File]) -> None:
|
|
108
|
-
self.
|
|
132
|
+
images = self.model.filter_images(files)
|
|
133
|
+
self._add(images, "Imagens selecionadas")
|
|
134
|
+
if self.auto_register:
|
|
135
|
+
self.register_named_files(images, "Imagens selecionadas")
|
|
109
136
|
|
|
110
137
|
def _add(self, found: ArrayList[File], origin: str) -> None:
|
|
111
138
|
known = {f.absolute() for f in self.files}
|
|
@@ -128,7 +155,9 @@ class ControllerImage(ChangeNotifier):
|
|
|
128
155
|
break
|
|
129
156
|
|
|
130
157
|
self._select_no_emit(idx)
|
|
131
|
-
self.set_status(
|
|
158
|
+
self.set_status(
|
|
159
|
+
f"{origin}: {len(new)} imagens adicionadas (total {self.total})."
|
|
160
|
+
)
|
|
132
161
|
self._emit()
|
|
133
162
|
self.uc_input.request_focus()
|
|
134
163
|
|
|
@@ -156,6 +185,103 @@ class ControllerImage(ChangeNotifier):
|
|
|
156
185
|
self._select_no_emit(min(idx, self.total - 1))
|
|
157
186
|
self._emit()
|
|
158
187
|
|
|
188
|
+
# ------------------------------------------------------------------ registrar nomeadas
|
|
189
|
+
|
|
190
|
+
def set_auto_register(self, v: bool) -> None:
|
|
191
|
+
self.auto_register = v # sem notify: não precisa redesenhar
|
|
192
|
+
|
|
193
|
+
def register_named_files(
|
|
194
|
+
self,
|
|
195
|
+
files: Iterable[File],
|
|
196
|
+
origin: str = "Arquivos",
|
|
197
|
+
organize: bool = False,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Registra no cache TOI (UC/TOI do nome + data do chat) em worker thread."""
|
|
200
|
+
files = list(files)
|
|
201
|
+
if not files:
|
|
202
|
+
return
|
|
203
|
+
if self._reg_thread is not None and self._reg_thread.is_alive():
|
|
204
|
+
self.show_info("Já existe um registro de cartas em andamento.")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
self.set_status(f"{origin}: verificando {len(files)} arquivos ...")
|
|
208
|
+
|
|
209
|
+
def progress(done: int, total: int) -> None:
|
|
210
|
+
if done == total or done % 20 == 0: # evita rebuild excessivo
|
|
211
|
+
_ui(
|
|
212
|
+
lambda: self.set_status(f"{origin}: registrando {done}/{total} ...")
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def work() -> None:
|
|
216
|
+
res: ResultRegisterNamed | None = None
|
|
217
|
+
err: Exception | None = None
|
|
218
|
+
try:
|
|
219
|
+
res = self.model.register_named(files, progress, organize)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
err = e
|
|
222
|
+
_ui(lambda: self._on_registered(origin, res, err))
|
|
223
|
+
|
|
224
|
+
self._reg_thread = threading.Thread(
|
|
225
|
+
target=work, daemon=True, name="image-register"
|
|
226
|
+
)
|
|
227
|
+
self._reg_thread.start()
|
|
228
|
+
|
|
229
|
+
def _on_registered(
|
|
230
|
+
self, origin: str, res: ResultRegisterNamed | None, err: Exception | None
|
|
231
|
+
) -> None:
|
|
232
|
+
if err is not None or res is None:
|
|
233
|
+
self.set_status(f"{origin}: falha ao registrar no cache TOI.")
|
|
234
|
+
self.show_error(f"Falha ao registrar no cache TOI:\n{err}")
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Arquivos movidos: atualiza a lista do visualizador para os novos caminhos.
|
|
238
|
+
if res.moves:
|
|
239
|
+
for i, f in enumerate(self.files):
|
|
240
|
+
new_path = res.moves.get(f.absolute())
|
|
241
|
+
if new_path is None:
|
|
242
|
+
continue
|
|
243
|
+
if f.absolute() in self.renamed:
|
|
244
|
+
self.renamed.discard(f.absolute())
|
|
245
|
+
self.renamed.add(new_path)
|
|
246
|
+
self.files[i] = File(new_path)
|
|
247
|
+
self.files_version += 1
|
|
248
|
+
self._emit()
|
|
249
|
+
|
|
250
|
+
moved_txt = f" | movidas: {res.moved}" if res.moved else ""
|
|
251
|
+
self.set_status(
|
|
252
|
+
f"{origin}: {res.recognized} cartas reconhecidas | "
|
|
253
|
+
f"{res.changed} gravadas/atualizadas no cache | "
|
|
254
|
+
f"com data: {res.with_date} | sem data: {res.without_date}"
|
|
255
|
+
f"{moved_txt}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if res.recognized == 0:
|
|
259
|
+
self.set_status(
|
|
260
|
+
f"{origin}: nenhum arquivo no padrão 'UC TOI' "
|
|
261
|
+
f"({res.total} verificados)."
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
self.set_status(
|
|
266
|
+
f"{origin}: {res.recognized} cartas reconhecidas | "
|
|
267
|
+
f"{res.changed} gravadas/atualizadas no cache | "
|
|
268
|
+
f"com data: {res.with_date} | sem data: {res.without_date}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
problems: list[str] = []
|
|
272
|
+
if res.without_date:
|
|
273
|
+
problems.append(
|
|
274
|
+
f"{res.without_date} carta(s) sem data (não encontradas na tabela WhatsApp). "
|
|
275
|
+
"Foram gravadas só com UC/TOI; importe de novo depois de atualizar o chat "
|
|
276
|
+
"para completar a data."
|
|
277
|
+
)
|
|
278
|
+
if res.failures:
|
|
279
|
+
sample = "\n".join(f"{p}: {e}" for p, e in res.failures[:5])
|
|
280
|
+
problems.append(f"{len(res.failures)} arquivo(s) com falha:\n{sample}")
|
|
281
|
+
problems.extend(res.warnings)
|
|
282
|
+
if problems:
|
|
283
|
+
self.show_info("\n\n".join(problems))
|
|
284
|
+
|
|
159
285
|
# ------------------------------------------------------------------ navegação
|
|
160
286
|
def select(self, idx: int) -> None:
|
|
161
287
|
self._select_no_emit(idx)
|