Carregar ficheiros para "/"
This commit is contained in:
206
README.md
Normal file
206
README.md
Normal file
@ -0,0 +1,206 @@
|
||||
# Informação Técnica Automóvel
|
||||
|
||||
Um sistema desktop robusto para gerenciar documentos técnicos automotivos, desenvolvido com Python e PySide6.
|
||||
|
||||
## 🆕 Atualizações Recentes
|
||||
|
||||
### Dezembro 2024
|
||||
- Renomeado o programa para "Informação Técnica Automóvel"
|
||||
- Adicionada funcionalidade de abrir documentos com duplo clique
|
||||
- Implementada busca avançada em Observações
|
||||
- Melhorada a associação de documentos com pastas específicas
|
||||
- Adicionado menu de contexto para documentos
|
||||
- Corrigida a visualização de documentos nas pastas corretas
|
||||
- Melhorada a interface de usuário com ordenação de colunas
|
||||
- Adicionado destaque visual para resultados de busca em Observações
|
||||
|
||||
### Recursos Atualizados
|
||||
- **Abertura de Documentos**:
|
||||
- Duplo clique para abrir documentos
|
||||
- Menu de contexto com opção "Abrir Documento"
|
||||
- Abertura automática no aplicativo padrão do sistema
|
||||
|
||||
- **Busca Aprimorada**:
|
||||
- Busca em todos os campos, incluindo Observações
|
||||
- Destaque visual para termos encontrados
|
||||
- Busca insensível a maiúsculas/minúsculas
|
||||
- Suporte a busca parcial de palavras
|
||||
|
||||
- **Organização de Pastas**:
|
||||
- Associação automática de documentos à pasta mais específica
|
||||
- Estrutura hierárquica de pastas aprimorada
|
||||
- Navegação intuitiva entre pastas
|
||||
|
||||
## 🚀 Funcionalidades
|
||||
|
||||
### 📁 Gerenciamento de Documentos
|
||||
- Upload individual e em lote de documentos
|
||||
- Organização em estrutura de pastas
|
||||
- Suporte para diversos formatos de arquivo (PDF, imagens, etc.)
|
||||
- Metadados detalhados (marca, modelo, ano, tipo, etc.)
|
||||
- Visualização integrada de documentos
|
||||
- Exportação e importação de documentos
|
||||
|
||||
### 🏷️ Organização e Categorização
|
||||
- Sistema de tags coloridas
|
||||
- Coleções personalizáveis
|
||||
- Categorização por marca/modelo
|
||||
- Estrutura hierárquica de pastas
|
||||
- Filtros avançados
|
||||
|
||||
### 📊 Versionamento e Histórico
|
||||
- Controle de versão de documentos
|
||||
- Histórico de alterações
|
||||
- Restauração de versões anteriores
|
||||
- Comparação entre versões
|
||||
- Registro de modificações
|
||||
|
||||
### 🔍 Busca e Visualização
|
||||
- Busca avançada por metadados
|
||||
- Visualização integrada de PDFs
|
||||
- Pré-visualização de imagens
|
||||
- Zoom e navegação de páginas
|
||||
- Comparação visual de documentos
|
||||
|
||||
### 💬 Colaboração
|
||||
- Sistema de comentários
|
||||
- Anotações por página
|
||||
- Tags compartilhadas
|
||||
- Coleções colaborativas
|
||||
- Exportação de relatórios
|
||||
|
||||
### 📈 Análise e Estatísticas
|
||||
- Dashboard de estatísticas
|
||||
- Gráficos de uso
|
||||
- Relatórios personalizados
|
||||
- Análise de espaço usado
|
||||
- Métricas de documentos
|
||||
|
||||
### ⚙️ Recursos Avançados
|
||||
- Backup automático
|
||||
- Preferências personalizáveis
|
||||
- Temas claro/escuro
|
||||
- Documentos recentes
|
||||
- Arrastar e soltar arquivos
|
||||
|
||||
## 🛠️ Tecnologias Utilizadas
|
||||
|
||||
- **Python**: Linguagem principal
|
||||
- **PySide6**: Interface gráfica
|
||||
- **SQLAlchemy**: ORM para banco de dados
|
||||
- **PyMuPDF**: Manipulação de PDFs
|
||||
- **OpenCV**: Processamento de imagens
|
||||
- **NumPy**: Operações numéricas
|
||||
- **Pillow**: Processamento de imagens
|
||||
|
||||
## 📋 Requisitos
|
||||
|
||||
- Python 3.8+
|
||||
- Dependências listadas em `requirements.txt`
|
||||
|
||||
## 🚀 Instalação
|
||||
|
||||
1. Clone o repositório:
|
||||
```bash
|
||||
git clone [URL_DO_REPOSITÓRIO]
|
||||
```
|
||||
|
||||
2. Instale as dependências:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Execute o aplicativo:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 🗂️ Estrutura do Projeto
|
||||
|
||||
```
|
||||
gestao-documentos/
|
||||
├── main.py # Ponto de entrada do aplicativo
|
||||
├── models.py # Modelos do banco de dados
|
||||
├── database.py # Configuração do banco de dados
|
||||
├── document_form.py # Formulário de documentos
|
||||
├── document_viewer.py # Visualizador de documentos
|
||||
├── folder_view.py # Visualização de pastas
|
||||
├── search_view.py # Interface de busca
|
||||
├── version_control.py # Controle de versão
|
||||
├── backup_manager.py # Gerenciamento de backup
|
||||
├── tag_manager.py # Gerenciamento de tags
|
||||
├── comments.py # Sistema de comentários
|
||||
├── collections.py # Sistema de coleções
|
||||
├── document_compare.py # Comparação de documentos
|
||||
├── statistics_view.py # Visualização de estatísticas
|
||||
├── preferences.py # Preferências do usuário
|
||||
└── requirements.txt # Dependências do projeto
|
||||
```
|
||||
|
||||
## 🎯 Principais Recursos
|
||||
|
||||
### Sistema de Arquivos
|
||||
- Estrutura hierárquica de pastas
|
||||
- Suporte para múltiplos formatos
|
||||
- Upload em lote
|
||||
- Organização flexível
|
||||
|
||||
### Metadados
|
||||
- Informações detalhadas do veículo
|
||||
- Dados técnicos do documento
|
||||
- Tags personalizáveis
|
||||
- Histórico de modificações
|
||||
|
||||
### Versionamento
|
||||
- Controle de versões
|
||||
- Histórico de alterações
|
||||
- Comparação entre versões
|
||||
- Restauração de versões
|
||||
|
||||
### Busca
|
||||
- Busca por metadados
|
||||
- Filtros avançados
|
||||
- Resultados categorizados
|
||||
- Histórico de busca
|
||||
|
||||
### Visualização
|
||||
- Visualizador integrado
|
||||
- Zoom e navegação
|
||||
- Anotações e marcações
|
||||
- Comparação lado a lado
|
||||
|
||||
### Organização
|
||||
- Sistema de tags
|
||||
- Coleções personalizadas
|
||||
- Categorização automática
|
||||
- Filtros inteligentes
|
||||
|
||||
### Backup
|
||||
- Exportação de documentos
|
||||
- Backup incremental
|
||||
- Restauração seletiva
|
||||
- Verificação de integridade
|
||||
|
||||
### Interface
|
||||
- Design moderno
|
||||
- Temas personalizáveis
|
||||
- Atalhos de teclado
|
||||
- Arrastar e soltar
|
||||
|
||||
## 🤝 Contribuição
|
||||
|
||||
1. Faça um Fork do projeto
|
||||
2. Crie uma Branch para sua Feature (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push para a Branch (`git push origin feature/AmazingFeature`)
|
||||
5. Abra um Pull Request
|
||||
|
||||
## 📝 Licença
|
||||
|
||||
Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
|
||||
|
||||
## 🎉 Agradecimentos
|
||||
|
||||
- Equipe de desenvolvimento
|
||||
- Contribuidores
|
||||
- Comunidade open source
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
PySide6>=6.5.0
|
||||
SQLAlchemy>=2.0.0
|
||||
PyMuPDF>=1.23.0
|
||||
opencv-python>=4.8.0
|
||||
numpy>=1.24.0
|
||||
Pillow>=10.0.0
|
||||
184
search_view.py
Normal file
184
search_view.py
Normal file
@ -0,0 +1,184 @@
|
||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLineEdit, QComboBox, QPushButton, QTableWidget,
|
||||
QTableWidgetItem, QSpinBox, QHeaderView, QMenu,
|
||||
QMessageBox)
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from database import get_session
|
||||
from models import Document
|
||||
import os
|
||||
from sqlalchemy import or_
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
class SearchView(QWidget):
|
||||
document_selected = Signal(int) # Signal emitted when document is selected
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Search form
|
||||
form_layout = QFormLayout()
|
||||
|
||||
# Search fields
|
||||
self.marca_edit = QLineEdit()
|
||||
self.modelo_edit = QLineEdit()
|
||||
self.ano_spin = QSpinBox()
|
||||
self.ano_spin.setRange(1900, 2100)
|
||||
self.ano_spin.setSpecialValueText(" ") # Empty string for no filter
|
||||
|
||||
self.tipo_combo = QComboBox()
|
||||
self.tipo_combo.addItems(["", "Elétrico", "Mecânico"])
|
||||
|
||||
self.variante_combo = QComboBox()
|
||||
self.variante_combo.addItems([
|
||||
"",
|
||||
"Esquema",
|
||||
"Esquema OEM",
|
||||
"TSB",
|
||||
"Esquemas Varios",
|
||||
"Documentação de Instruções"
|
||||
])
|
||||
|
||||
self.observacoes_edit = QLineEdit()
|
||||
self.observacoes_edit.setPlaceholderText("Digite sua busca...")
|
||||
|
||||
form_layout.addRow("Marca:", self.marca_edit)
|
||||
form_layout.addRow("Modelo:", self.modelo_edit)
|
||||
form_layout.addRow("Ano:", self.ano_spin)
|
||||
form_layout.addRow("Tipo:", self.tipo_combo)
|
||||
form_layout.addRow("Variante:", self.variante_combo)
|
||||
form_layout.addRow("Observações:", self.observacoes_edit)
|
||||
|
||||
# Search button
|
||||
button_layout = QHBoxLayout()
|
||||
self.search_button = QPushButton("Pesquisar")
|
||||
self.search_button.clicked.connect(self.perform_search)
|
||||
self.clear_button = QPushButton("Limpar")
|
||||
self.clear_button.clicked.connect(self.clear_search)
|
||||
button_layout.addWidget(self.search_button)
|
||||
button_layout.addWidget(self.clear_button)
|
||||
|
||||
# Add layouts to main layout
|
||||
layout.addLayout(form_layout)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Results table
|
||||
self.results_table = QTableWidget()
|
||||
self.results_table.setColumnCount(7)
|
||||
self.results_table.setHorizontalHeaderLabels([
|
||||
"Marca", "Modelo", "Ano", "Tipo", "Variante",
|
||||
"Nome do Arquivo", "Observações"
|
||||
])
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeToContents)
|
||||
self.results_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.results_table.customContextMenuRequested.connect(
|
||||
self.show_context_menu)
|
||||
layout.addWidget(self.results_table)
|
||||
|
||||
def perform_search(self):
|
||||
session = get_session()
|
||||
query = session.query(Document)
|
||||
|
||||
# Apply filters
|
||||
if self.marca_edit.text():
|
||||
query = query.filter(Document.marca.ilike(f"%{self.marca_edit.text()}%"))
|
||||
|
||||
if self.modelo_edit.text():
|
||||
query = query.filter(Document.modelo.ilike(f"%{self.modelo_edit.text()}%"))
|
||||
|
||||
if self.ano_spin.value() != self.ano_spin.minimum():
|
||||
query = query.filter(Document.ano == self.ano_spin.value())
|
||||
|
||||
if self.tipo_combo.currentText():
|
||||
query = query.filter(Document.tipo_documento == self.tipo_combo.currentText())
|
||||
|
||||
if self.variante_combo.currentText():
|
||||
query = query.filter(Document.variante == self.variante_combo.currentText())
|
||||
|
||||
if self.observacoes_edit.text():
|
||||
query = query.filter(Document.observacoes.ilike(f"%{self.observacoes_edit.text()}%"))
|
||||
|
||||
# Get results
|
||||
documents = query.all()
|
||||
|
||||
# Update table
|
||||
valid_documents = []
|
||||
for doc in documents:
|
||||
if os.path.exists(doc.file_path):
|
||||
valid_documents.append(doc)
|
||||
else:
|
||||
# Optional: Remove documents with missing files
|
||||
session.delete(doc)
|
||||
|
||||
if len(documents) != len(valid_documents):
|
||||
session.commit()
|
||||
documents = valid_documents
|
||||
|
||||
self.results_table.setRowCount(len(documents))
|
||||
for row, doc in enumerate(documents):
|
||||
self.results_table.setItem(row, 0, QTableWidgetItem(doc.marca))
|
||||
self.results_table.setItem(row, 1, QTableWidgetItem(doc.modelo))
|
||||
self.results_table.setItem(row, 2, QTableWidgetItem(str(doc.ano)))
|
||||
self.results_table.setItem(row, 3, QTableWidgetItem(doc.tipo_documento))
|
||||
self.results_table.setItem(row, 4, QTableWidgetItem(doc.variante))
|
||||
self.results_table.setItem(row, 5, QTableWidgetItem(doc.file_name))
|
||||
self.results_table.setItem(row, 6, QTableWidgetItem(doc.observacoes))
|
||||
|
||||
# Store document ID in the first column
|
||||
self.results_table.item(row, 0).setData(Qt.UserRole, doc.id)
|
||||
|
||||
session.close()
|
||||
|
||||
def clear_search(self):
|
||||
self.marca_edit.clear()
|
||||
self.modelo_edit.clear()
|
||||
self.ano_spin.setValue(self.ano_spin.minimum())
|
||||
self.tipo_combo.setCurrentIndex(0)
|
||||
self.variante_combo.setCurrentIndex(0)
|
||||
self.observacoes_edit.clear()
|
||||
self.results_table.setRowCount(0)
|
||||
|
||||
def show_context_menu(self, position):
|
||||
menu = QMenu()
|
||||
open_action = menu.addAction("Abrir Documento")
|
||||
open_folder_action = menu.addAction("Abrir Pasta")
|
||||
|
||||
action = menu.exec_(self.results_table.mapToGlobal(position))
|
||||
|
||||
current_row = self.results_table.currentRow()
|
||||
if current_row >= 0:
|
||||
doc_id = self.results_table.item(current_row, 0).data(Qt.UserRole)
|
||||
|
||||
if action == open_action:
|
||||
self.open_document(doc_id)
|
||||
elif action == open_folder_action:
|
||||
self.open_document_folder(doc_id)
|
||||
|
||||
def open_document(self, doc_id):
|
||||
session = get_session()
|
||||
document = session.query(Document).get(doc_id)
|
||||
|
||||
if document and os.path.exists(document.file_path):
|
||||
self.document_selected.emit(doc_id)
|
||||
else:
|
||||
QMessageBox.warning(self, "Erro",
|
||||
"Não foi possível abrir o documento.")
|
||||
|
||||
session.close()
|
||||
|
||||
def open_document_folder(self, doc_id):
|
||||
session = get_session()
|
||||
document = session.query(Document).get(doc_id)
|
||||
|
||||
if document and os.path.exists(document.file_path):
|
||||
folder_path = os.path.dirname(document.file_path)
|
||||
os.startfile(folder_path)
|
||||
else:
|
||||
QMessageBox.warning(self, "Erro",
|
||||
"Não foi possível abrir a pasta do documento.")
|
||||
|
||||
session.close()
|
||||
130
statistics_view.py
Normal file
130
statistics_view.py
Normal file
@ -0,0 +1,130 @@
|
||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QLabel, QTableWidget, QTableWidgetItem,
|
||||
QHeaderView)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet
|
||||
from PySide6.QtGui import QPainter
|
||||
from database import get_session
|
||||
from models import Document, Folder, Statistics
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
class StatisticsView(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Refresh button
|
||||
self.refresh_btn = QPushButton("Atualizar Estatísticas")
|
||||
self.refresh_btn.clicked.connect(self.update_statistics)
|
||||
layout.addWidget(self.refresh_btn)
|
||||
|
||||
# Summary layout
|
||||
summary_layout = QHBoxLayout()
|
||||
|
||||
# Total documents
|
||||
self.total_docs_label = QLabel("Total de Documentos: 0")
|
||||
summary_layout.addWidget(self.total_docs_label)
|
||||
|
||||
# Total folders
|
||||
self.total_folders_label = QLabel("Total de Pastas: 0")
|
||||
summary_layout.addWidget(self.total_folders_label)
|
||||
|
||||
# Storage used
|
||||
self.storage_label = QLabel("Espaço Utilizado: 0 MB")
|
||||
summary_layout.addWidget(self.storage_label)
|
||||
|
||||
layout.addLayout(summary_layout)
|
||||
|
||||
# Charts layout
|
||||
charts_layout = QHBoxLayout()
|
||||
|
||||
# Document types chart
|
||||
self.types_chart = QChart()
|
||||
self.types_chart.setTitle("Documentos por Tipo")
|
||||
types_view = QChartView(self.types_chart)
|
||||
types_view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
charts_layout.addWidget(types_view)
|
||||
|
||||
# Brands chart
|
||||
self.brands_chart = QChart()
|
||||
self.brands_chart.setTitle("Documentos por Marca")
|
||||
brands_view = QChartView(self.brands_chart)
|
||||
brands_view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
charts_layout.addWidget(brands_view)
|
||||
|
||||
layout.addLayout(charts_layout)
|
||||
|
||||
self.update_statistics()
|
||||
|
||||
def update_statistics(self):
|
||||
session = get_session()
|
||||
|
||||
try:
|
||||
# Count documents and folders
|
||||
total_docs = session.query(Document).count()
|
||||
total_folders = session.query(Folder).count()
|
||||
|
||||
# Calculate storage used
|
||||
storage_used = 0
|
||||
for doc in session.query(Document).all():
|
||||
if os.path.exists(doc.file_path):
|
||||
storage_used += os.path.getsize(doc.file_path)
|
||||
for version in doc.versions:
|
||||
if os.path.exists(version.file_path):
|
||||
storage_used += os.path.getsize(version.file_path)
|
||||
|
||||
# Update labels
|
||||
self.total_docs_label.setText(f"Total de Documentos: {total_docs}")
|
||||
self.total_folders_label.setText(f"Total de Pastas: {total_folders}")
|
||||
self.storage_label.setText(
|
||||
f"Espaço Utilizado: {storage_used / (1024*1024):.2f} MB")
|
||||
|
||||
# Document types statistics
|
||||
types_series = QPieSeries()
|
||||
types_count = {}
|
||||
for doc in session.query(Document).all():
|
||||
types_count[doc.tipo_documento] = types_count.get(
|
||||
doc.tipo_documento, 0) + 1
|
||||
|
||||
for doc_type, count in types_count.items():
|
||||
types_series.append(f"{doc_type} ({count})", count)
|
||||
|
||||
self.types_chart.removeAllSeries()
|
||||
self.types_chart.addSeries(types_series)
|
||||
|
||||
# Brands statistics
|
||||
brands_series = QBarSeries()
|
||||
brands_count = {}
|
||||
for doc in session.query(Document).all():
|
||||
brands_count[doc.marca] = brands_count.get(doc.marca, 0) + 1
|
||||
|
||||
brands_set = QBarSet("Marcas")
|
||||
brands = list(brands_count.keys())
|
||||
values = [brands_count[brand] for brand in brands]
|
||||
brands_set.append(values)
|
||||
|
||||
brands_series.append(brands_set)
|
||||
self.brands_chart.removeAllSeries()
|
||||
self.brands_chart.addSeries(brands_series)
|
||||
|
||||
# Save statistics
|
||||
stats = Statistics(
|
||||
date=datetime.utcnow(),
|
||||
total_documents=total_docs,
|
||||
total_folders=total_folders,
|
||||
documents_by_type=json.dumps(types_count),
|
||||
documents_by_brand=json.dumps(brands_count),
|
||||
storage_used=storage_used
|
||||
)
|
||||
session.add(stats)
|
||||
session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating statistics: {str(e)}")
|
||||
finally:
|
||||
session.close()
|
||||
246
tag_manager.py
Normal file
246
tag_manager.py
Normal file
@ -0,0 +1,246 @@
|
||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QMessageBox, QInputDialog, QColorDialog, QLabel)
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from database import get_session
|
||||
from models import Tag, Document
|
||||
import json
|
||||
|
||||
class TagManager(QWidget):
|
||||
tags_updated = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
self.add_tag_btn = QPushButton("Nova Tag")
|
||||
self.add_tag_btn.clicked.connect(self.add_tag)
|
||||
self.edit_tag_btn = QPushButton("Editar Tag")
|
||||
self.edit_tag_btn.clicked.connect(self.edit_tag)
|
||||
self.delete_tag_btn = QPushButton("Excluir Tag")
|
||||
self.delete_tag_btn.clicked.connect(self.delete_tag)
|
||||
|
||||
button_layout.addWidget(self.add_tag_btn)
|
||||
button_layout.addWidget(self.edit_tag_btn)
|
||||
button_layout.addWidget(self.delete_tag_btn)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Tags table
|
||||
self.tags_table = QTableWidget()
|
||||
self.tags_table.setColumnCount(3)
|
||||
self.tags_table.setHorizontalHeaderLabels([
|
||||
"Nome", "Cor", "Documentos"
|
||||
])
|
||||
header = self.tags_table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||
layout.addWidget(self.tags_table)
|
||||
|
||||
self.refresh_tags()
|
||||
|
||||
def refresh_tags(self):
|
||||
session = get_session()
|
||||
tags = session.query(Tag).all()
|
||||
|
||||
self.tags_table.setRowCount(len(tags))
|
||||
for row, tag in enumerate(tags):
|
||||
# Name
|
||||
self.tags_table.setItem(row, 0, QTableWidgetItem(tag.name))
|
||||
|
||||
# Color
|
||||
color_item = QTableWidgetItem()
|
||||
color_item.setBackground(QColor(tag.color))
|
||||
self.tags_table.setItem(row, 1, color_item)
|
||||
|
||||
# Document count
|
||||
doc_count = len(tag.documents)
|
||||
self.tags_table.setItem(row, 2, QTableWidgetItem(str(doc_count)))
|
||||
|
||||
session.close()
|
||||
|
||||
def add_tag(self):
|
||||
name, ok = QInputDialog.getText(
|
||||
self,
|
||||
"Nova Tag",
|
||||
"Nome da tag:"
|
||||
)
|
||||
|
||||
if ok and name:
|
||||
color = QColorDialog.getColor()
|
||||
if color.isValid():
|
||||
try:
|
||||
session = get_session()
|
||||
|
||||
# Check if tag already exists
|
||||
if session.query(Tag).filter(Tag.name == name).first():
|
||||
QMessageBox.warning(self, "Erro",
|
||||
"Uma tag com este nome já existe.")
|
||||
return
|
||||
|
||||
new_tag = Tag(
|
||||
name=name,
|
||||
color=color.name()
|
||||
)
|
||||
session.add(new_tag)
|
||||
session.commit()
|
||||
|
||||
self.refresh_tags()
|
||||
self.tags_updated.emit()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erro",
|
||||
f"Erro ao criar tag: {str(e)}")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def edit_tag(self):
|
||||
current_row = self.tags_table.currentRow()
|
||||
if current_row < 0:
|
||||
return
|
||||
|
||||
current_name = self.tags_table.item(current_row, 0).text()
|
||||
new_name, ok = QInputDialog.getText(
|
||||
self,
|
||||
"Editar Tag",
|
||||
"Nome da tag:",
|
||||
text=current_name
|
||||
)
|
||||
|
||||
if ok and new_name:
|
||||
color = QColorDialog.getColor()
|
||||
if color.isValid():
|
||||
try:
|
||||
session = get_session()
|
||||
tag = session.query(Tag).filter(Tag.name == current_name).first()
|
||||
|
||||
if tag:
|
||||
tag.name = new_name
|
||||
tag.color = color.name()
|
||||
session.commit()
|
||||
|
||||
self.refresh_tags()
|
||||
self.tags_updated.emit()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erro",
|
||||
f"Erro ao editar tag: {str(e)}")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def delete_tag(self):
|
||||
current_row = self.tags_table.currentRow()
|
||||
if current_row < 0:
|
||||
return
|
||||
|
||||
tag_name = self.tags_table.item(current_row, 0).text()
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirmar Exclusão",
|
||||
f"Tem certeza que deseja excluir a tag '{tag_name}'?",
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
try:
|
||||
session = get_session()
|
||||
tag = session.query(Tag).filter(Tag.name == tag_name).first()
|
||||
|
||||
if tag:
|
||||
session.delete(tag)
|
||||
session.commit()
|
||||
|
||||
self.refresh_tags()
|
||||
self.tags_updated.emit()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Erro",
|
||||
f"Erro ao excluir tag: {str(e)}")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
class DocumentTags(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_document = None
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
self.tags_layout = QHBoxLayout()
|
||||
layout.addLayout(self.tags_layout)
|
||||
|
||||
# Add tag button
|
||||
self.add_tag_btn = QPushButton("Adicionar Tag")
|
||||
self.add_tag_btn.clicked.connect(self.add_tag_to_document)
|
||||
layout.addWidget(self.add_tag_btn)
|
||||
|
||||
def load_document(self, doc_id):
|
||||
session = get_session()
|
||||
self.current_document = session.query(Document).get(doc_id)
|
||||
self.refresh_tags()
|
||||
session.close()
|
||||
|
||||
def refresh_tags(self):
|
||||
# Clear existing tags
|
||||
for i in reversed(range(self.tags_layout.count())):
|
||||
self.tags_layout.itemAt(i).widget().setParent(None)
|
||||
|
||||
if self.current_document:
|
||||
session = get_session()
|
||||
document = session.query(Document).get(self.current_document.id)
|
||||
|
||||
for tag in document.tags:
|
||||
self.add_tag_label(tag)
|
||||
|
||||
session.close()
|
||||
|
||||
def add_tag_label(self, tag):
|
||||
label = QLabel(tag.name)
|
||||
label.setStyleSheet(
|
||||
f"background-color: {tag.color}; padding: 2px 5px; border-radius: 3px;"
|
||||
)
|
||||
self.tags_layout.addWidget(label)
|
||||
|
||||
def add_tag_to_document(self):
|
||||
if not self.current_document:
|
||||
return
|
||||
|
||||
session = get_session()
|
||||
available_tags = session.query(Tag).all()
|
||||
|
||||
if not available_tags:
|
||||
QMessageBox.warning(self, "Aviso",
|
||||
"Não há tags disponíveis. Crie algumas tags primeiro.")
|
||||
return
|
||||
|
||||
tag_names = [tag.name for tag in available_tags]
|
||||
current_tags = [tag.name for tag in self.current_document.tags]
|
||||
available_names = [n for n in tag_names if n not in current_tags]
|
||||
|
||||
if not available_names:
|
||||
QMessageBox.information(self, "Aviso",
|
||||
"Todas as tags já foram aplicadas a este documento.")
|
||||
return
|
||||
|
||||
name, ok = QInputDialog.getItem(
|
||||
self,
|
||||
"Adicionar Tag",
|
||||
"Selecione uma tag:",
|
||||
available_names,
|
||||
0,
|
||||
False
|
||||
)
|
||||
|
||||
if ok and name:
|
||||
tag = session.query(Tag).filter(Tag.name == name).first()
|
||||
document = session.query(Document).get(self.current_document.id)
|
||||
document.tags.append(tag)
|
||||
session.commit()
|
||||
self.refresh_tags()
|
||||
|
||||
session.close()
|
||||
Reference in New Issue
Block a user