supervertaler 1.9.163__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.
- Supervertaler.py +48473 -0
- modules/__init__.py +10 -0
- modules/ai_actions.py +964 -0
- modules/ai_attachment_manager.py +343 -0
- modules/ai_file_viewer_dialog.py +210 -0
- modules/autofingers_engine.py +466 -0
- modules/cafetran_docx_handler.py +379 -0
- modules/config_manager.py +469 -0
- modules/database_manager.py +1911 -0
- modules/database_migrations.py +417 -0
- modules/dejavurtf_handler.py +779 -0
- modules/document_analyzer.py +427 -0
- modules/docx_handler.py +689 -0
- modules/encoding_repair.py +319 -0
- modules/encoding_repair_Qt.py +393 -0
- modules/encoding_repair_ui.py +481 -0
- modules/feature_manager.py +350 -0
- modules/figure_context_manager.py +340 -0
- modules/file_dialog_helper.py +148 -0
- modules/find_replace.py +164 -0
- modules/find_replace_qt.py +457 -0
- modules/glossary_manager.py +433 -0
- modules/image_extractor.py +188 -0
- modules/keyboard_shortcuts_widget.py +571 -0
- modules/llm_clients.py +1211 -0
- modules/llm_leaderboard.py +737 -0
- modules/llm_superbench_ui.py +1401 -0
- modules/local_llm_setup.py +1104 -0
- modules/model_update_dialog.py +381 -0
- modules/model_version_checker.py +373 -0
- modules/mqxliff_handler.py +638 -0
- modules/non_translatables_manager.py +743 -0
- modules/pdf_rescue_Qt.py +1822 -0
- modules/pdf_rescue_tkinter.py +909 -0
- modules/phrase_docx_handler.py +516 -0
- modules/project_home_panel.py +209 -0
- modules/prompt_assistant.py +357 -0
- modules/prompt_library.py +689 -0
- modules/prompt_library_migration.py +447 -0
- modules/quick_access_sidebar.py +282 -0
- modules/ribbon_widget.py +597 -0
- modules/sdlppx_handler.py +874 -0
- modules/setup_wizard.py +353 -0
- modules/shortcut_manager.py +932 -0
- modules/simple_segmenter.py +128 -0
- modules/spellcheck_manager.py +727 -0
- modules/statuses.py +207 -0
- modules/style_guide_manager.py +315 -0
- modules/superbench_ui.py +1319 -0
- modules/superbrowser.py +329 -0
- modules/supercleaner.py +600 -0
- modules/supercleaner_ui.py +444 -0
- modules/superdocs.py +19 -0
- modules/superdocs_viewer_qt.py +382 -0
- modules/superlookup.py +252 -0
- modules/tag_cleaner.py +260 -0
- modules/tag_manager.py +351 -0
- modules/term_extractor.py +270 -0
- modules/termbase_entry_editor.py +842 -0
- modules/termbase_import_export.py +488 -0
- modules/termbase_manager.py +1060 -0
- modules/termview_widget.py +1176 -0
- modules/theme_manager.py +499 -0
- modules/tm_editor_dialog.py +99 -0
- modules/tm_manager_qt.py +1280 -0
- modules/tm_metadata_manager.py +545 -0
- modules/tmx_editor.py +1461 -0
- modules/tmx_editor_qt.py +2784 -0
- modules/tmx_generator.py +284 -0
- modules/tracked_changes.py +900 -0
- modules/trados_docx_handler.py +430 -0
- modules/translation_memory.py +715 -0
- modules/translation_results_panel.py +2134 -0
- modules/translation_services.py +282 -0
- modules/unified_prompt_library.py +659 -0
- modules/unified_prompt_manager_qt.py +3951 -0
- modules/voice_commands.py +920 -0
- modules/voice_dictation.py +477 -0
- modules/voice_dictation_lite.py +249 -0
- supervertaler-1.9.163.dist-info/METADATA +906 -0
- supervertaler-1.9.163.dist-info/RECORD +85 -0
- supervertaler-1.9.163.dist-info/WHEEL +5 -0
- supervertaler-1.9.163.dist-info/entry_points.txt +2 -0
- supervertaler-1.9.163.dist-info/licenses/LICENSE +21 -0
- supervertaler-1.9.163.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Assistant Attachment Manager
|
|
3
|
+
|
|
4
|
+
Manages persistent storage of attached files for the AI Assistant.
|
|
5
|
+
Files are converted to markdown and stored with metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import hashlib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AttachmentManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages file attachments for AI Assistant conversations.
|
|
19
|
+
|
|
20
|
+
Features:
|
|
21
|
+
- Persistent storage of converted markdown files
|
|
22
|
+
- Metadata tracking (original name, path, type, size, date)
|
|
23
|
+
- Session-based organization
|
|
24
|
+
- Master index for quick lookup
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_dir: str = None, log_callback=None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the AttachmentManager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
base_dir: Base directory for attachments (default: user_data_private/ai_assistant)
|
|
33
|
+
log_callback: Function to call for logging messages
|
|
34
|
+
"""
|
|
35
|
+
self.log = log_callback if log_callback else print
|
|
36
|
+
|
|
37
|
+
# Set base directory
|
|
38
|
+
if base_dir is None:
|
|
39
|
+
# Default to user_data_private/ai_assistant
|
|
40
|
+
base_dir = Path("user_data_private") / "ai_assistant"
|
|
41
|
+
|
|
42
|
+
self.base_dir = Path(base_dir)
|
|
43
|
+
self.attachments_dir = self.base_dir / "attachments"
|
|
44
|
+
self.conversations_dir = self.base_dir / "conversations"
|
|
45
|
+
self.index_file = self.base_dir / "index.json"
|
|
46
|
+
|
|
47
|
+
# Create directory structure
|
|
48
|
+
self._init_directories()
|
|
49
|
+
|
|
50
|
+
# Load index
|
|
51
|
+
self.index = self._load_index()
|
|
52
|
+
|
|
53
|
+
# Current session ID
|
|
54
|
+
self.current_session_id = None
|
|
55
|
+
|
|
56
|
+
def _init_directories(self):
|
|
57
|
+
"""Create necessary directories if they don't exist"""
|
|
58
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
self.attachments_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
self.conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
self.log(f"✓ Attachment directories initialized: {self.base_dir}")
|
|
63
|
+
|
|
64
|
+
def _load_index(self) -> Dict:
|
|
65
|
+
"""Load the master index of all attachments"""
|
|
66
|
+
if not self.index_file.exists():
|
|
67
|
+
return {
|
|
68
|
+
"version": "1.0",
|
|
69
|
+
"created": datetime.now().isoformat(),
|
|
70
|
+
"attachments": {}, # file_id -> metadata
|
|
71
|
+
"sessions": {} # session_id -> [file_ids]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
with open(self.index_file, 'r', encoding='utf-8') as f:
|
|
76
|
+
return json.load(f)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.log(f"⚠ Failed to load index: {e}")
|
|
79
|
+
return {
|
|
80
|
+
"version": "1.0",
|
|
81
|
+
"created": datetime.now().isoformat(),
|
|
82
|
+
"attachments": {},
|
|
83
|
+
"sessions": {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def _save_index(self):
|
|
87
|
+
"""Save the master index"""
|
|
88
|
+
try:
|
|
89
|
+
self.index["updated"] = datetime.now().isoformat()
|
|
90
|
+
with open(self.index_file, 'w', encoding='utf-8') as f:
|
|
91
|
+
json.dump(self.index, f, indent=2, ensure_ascii=False)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.log(f"✗ Failed to save index: {e}")
|
|
94
|
+
|
|
95
|
+
def _generate_file_id(self, original_path: str, content: str) -> str:
|
|
96
|
+
"""Generate unique file ID based on path and content hash"""
|
|
97
|
+
content_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()[:16]
|
|
98
|
+
path_hash = hashlib.sha256(original_path.encode('utf-8')).hexdigest()[:8]
|
|
99
|
+
return f"{path_hash}_{content_hash}"
|
|
100
|
+
|
|
101
|
+
def set_session(self, session_id: str):
|
|
102
|
+
"""Set the current session ID"""
|
|
103
|
+
self.current_session_id = session_id
|
|
104
|
+
|
|
105
|
+
# Create session directory
|
|
106
|
+
session_dir = self.attachments_dir / session_id
|
|
107
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
|
|
109
|
+
# Initialize session in index
|
|
110
|
+
if session_id not in self.index["sessions"]:
|
|
111
|
+
self.index["sessions"][session_id] = []
|
|
112
|
+
self._save_index()
|
|
113
|
+
|
|
114
|
+
def attach_file(
|
|
115
|
+
self,
|
|
116
|
+
original_path: str,
|
|
117
|
+
markdown_content: str,
|
|
118
|
+
original_name: str = None,
|
|
119
|
+
conversation_id: str = None
|
|
120
|
+
) -> Optional[str]:
|
|
121
|
+
"""
|
|
122
|
+
Save an attached file with metadata.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
original_path: Full path to original file
|
|
126
|
+
markdown_content: Converted markdown content
|
|
127
|
+
original_name: Original filename (optional, extracted from path if not provided)
|
|
128
|
+
conversation_id: ID of conversation this file belongs to
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
file_id if successful, None otherwise
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
# Ensure session is set
|
|
135
|
+
if self.current_session_id is None:
|
|
136
|
+
self.set_session(datetime.now().strftime("%Y%m%d_%H%M%S"))
|
|
137
|
+
|
|
138
|
+
# Extract filename if not provided
|
|
139
|
+
if original_name is None:
|
|
140
|
+
original_name = Path(original_path).name
|
|
141
|
+
|
|
142
|
+
# Generate file ID
|
|
143
|
+
file_id = self._generate_file_id(original_path, markdown_content)
|
|
144
|
+
|
|
145
|
+
# Check if already attached
|
|
146
|
+
if file_id in self.index["attachments"]:
|
|
147
|
+
self.log(f"⚠ File already attached: {original_name}")
|
|
148
|
+
return file_id
|
|
149
|
+
|
|
150
|
+
# Session directory
|
|
151
|
+
session_dir = self.attachments_dir / self.current_session_id
|
|
152
|
+
|
|
153
|
+
# Save markdown content
|
|
154
|
+
md_file = session_dir / f"{file_id}.md"
|
|
155
|
+
md_file.write_text(markdown_content, encoding='utf-8')
|
|
156
|
+
|
|
157
|
+
# Create metadata
|
|
158
|
+
file_type = Path(original_path).suffix
|
|
159
|
+
metadata = {
|
|
160
|
+
"file_id": file_id,
|
|
161
|
+
"original_name": original_name,
|
|
162
|
+
"original_path": str(original_path),
|
|
163
|
+
"file_type": file_type,
|
|
164
|
+
"size_bytes": len(markdown_content.encode('utf-8')),
|
|
165
|
+
"size_chars": len(markdown_content),
|
|
166
|
+
"attached_at": datetime.now().isoformat(),
|
|
167
|
+
"session_id": self.current_session_id,
|
|
168
|
+
"conversation_id": conversation_id,
|
|
169
|
+
"markdown_path": str(md_file.relative_to(self.base_dir))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Save metadata
|
|
173
|
+
meta_file = session_dir / f"{file_id}.meta.json"
|
|
174
|
+
with open(meta_file, 'w', encoding='utf-8') as f:
|
|
175
|
+
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
|
176
|
+
|
|
177
|
+
# Update index
|
|
178
|
+
self.index["attachments"][file_id] = metadata
|
|
179
|
+
self.index["sessions"][self.current_session_id].append(file_id)
|
|
180
|
+
self._save_index()
|
|
181
|
+
|
|
182
|
+
self.log(f"✓ Attached file: {original_name} ({file_id})")
|
|
183
|
+
return file_id
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
self.log(f"✗ Failed to attach file: {e}")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def get_file(self, file_id: str) -> Optional[Dict]:
|
|
190
|
+
"""
|
|
191
|
+
Get file metadata and content.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
file_id: File ID
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dictionary with metadata and content, or None if not found
|
|
198
|
+
"""
|
|
199
|
+
if file_id not in self.index["attachments"]:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
metadata = self.index["attachments"][file_id].copy()
|
|
203
|
+
|
|
204
|
+
# Load markdown content
|
|
205
|
+
md_path = self.base_dir / metadata["markdown_path"]
|
|
206
|
+
if md_path.exists():
|
|
207
|
+
metadata["content"] = md_path.read_text(encoding='utf-8')
|
|
208
|
+
else:
|
|
209
|
+
metadata["content"] = None
|
|
210
|
+
|
|
211
|
+
return metadata
|
|
212
|
+
|
|
213
|
+
def remove_file(self, file_id: str) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Remove an attached file.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
file_id: File ID to remove
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if successful, False otherwise
|
|
222
|
+
"""
|
|
223
|
+
if file_id not in self.index["attachments"]:
|
|
224
|
+
self.log(f"⚠ File not found: {file_id}")
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
metadata = self.index["attachments"][file_id]
|
|
229
|
+
session_id = metadata["session_id"]
|
|
230
|
+
|
|
231
|
+
# Delete files
|
|
232
|
+
session_dir = self.attachments_dir / session_id
|
|
233
|
+
md_file = session_dir / f"{file_id}.md"
|
|
234
|
+
meta_file = session_dir / f"{file_id}.meta.json"
|
|
235
|
+
|
|
236
|
+
if md_file.exists():
|
|
237
|
+
md_file.unlink()
|
|
238
|
+
if meta_file.exists():
|
|
239
|
+
meta_file.unlink()
|
|
240
|
+
|
|
241
|
+
# Update index
|
|
242
|
+
del self.index["attachments"][file_id]
|
|
243
|
+
if session_id in self.index["sessions"]:
|
|
244
|
+
if file_id in self.index["sessions"][session_id]:
|
|
245
|
+
self.index["sessions"][session_id].remove(file_id)
|
|
246
|
+
|
|
247
|
+
self._save_index()
|
|
248
|
+
|
|
249
|
+
self.log(f"✓ Removed file: {file_id}")
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
self.log(f"✗ Failed to remove file: {e}")
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def list_session_files(self, session_id: str = None) -> List[Dict]:
|
|
257
|
+
"""
|
|
258
|
+
List all files in a session.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
session_id: Session ID (uses current session if None)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of file metadata dictionaries
|
|
265
|
+
"""
|
|
266
|
+
if session_id is None:
|
|
267
|
+
session_id = self.current_session_id
|
|
268
|
+
|
|
269
|
+
if session_id is None or session_id not in self.index["sessions"]:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
file_ids = self.index["sessions"][session_id]
|
|
273
|
+
files = []
|
|
274
|
+
|
|
275
|
+
for file_id in file_ids:
|
|
276
|
+
if file_id in self.index["attachments"]:
|
|
277
|
+
files.append(self.index["attachments"][file_id].copy())
|
|
278
|
+
|
|
279
|
+
# Sort by attached_at (most recent first)
|
|
280
|
+
files.sort(key=lambda x: x.get("attached_at", ""), reverse=True)
|
|
281
|
+
|
|
282
|
+
return files
|
|
283
|
+
|
|
284
|
+
def list_all_files(self) -> List[Dict]:
|
|
285
|
+
"""
|
|
286
|
+
List all attached files across all sessions.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
List of file metadata dictionaries
|
|
290
|
+
"""
|
|
291
|
+
files = [
|
|
292
|
+
metadata.copy()
|
|
293
|
+
for metadata in self.index["attachments"].values()
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
# Sort by attached_at (most recent first)
|
|
297
|
+
files.sort(key=lambda x: x.get("attached_at", ""), reverse=True)
|
|
298
|
+
|
|
299
|
+
return files
|
|
300
|
+
|
|
301
|
+
def get_stats(self) -> Dict:
|
|
302
|
+
"""
|
|
303
|
+
Get statistics about attachments.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Dictionary with stats (total_files, total_size, sessions, etc.)
|
|
307
|
+
"""
|
|
308
|
+
total_files = len(self.index["attachments"])
|
|
309
|
+
total_size = sum(
|
|
310
|
+
meta.get("size_bytes", 0)
|
|
311
|
+
for meta in self.index["attachments"].values()
|
|
312
|
+
)
|
|
313
|
+
total_sessions = len(self.index["sessions"])
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"total_files": total_files,
|
|
317
|
+
"total_size_bytes": total_size,
|
|
318
|
+
"total_size_mb": total_size / (1024 * 1024),
|
|
319
|
+
"total_sessions": total_sessions,
|
|
320
|
+
"current_session": self.current_session_id
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
def cleanup_empty_sessions(self):
|
|
324
|
+
"""Remove sessions with no files"""
|
|
325
|
+
empty_sessions = [
|
|
326
|
+
session_id
|
|
327
|
+
for session_id, file_ids in self.index["sessions"].items()
|
|
328
|
+
if len(file_ids) == 0
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
for session_id in empty_sessions:
|
|
332
|
+
del self.index["sessions"][session_id]
|
|
333
|
+
|
|
334
|
+
# Remove session directory if empty
|
|
335
|
+
session_dir = self.attachments_dir / session_id
|
|
336
|
+
if session_dir.exists() and not any(session_dir.iterdir()):
|
|
337
|
+
session_dir.rmdir()
|
|
338
|
+
|
|
339
|
+
if empty_sessions:
|
|
340
|
+
self._save_index()
|
|
341
|
+
self.log(f"✓ Cleaned up {len(empty_sessions)} empty sessions")
|
|
342
|
+
|
|
343
|
+
return len(empty_sessions)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Assistant File Viewer Dialog
|
|
3
|
+
|
|
4
|
+
Dialog for viewing attached file content in markdown format.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit,
|
|
9
|
+
QPushButton, QGroupBox, QMessageBox, QApplication
|
|
10
|
+
)
|
|
11
|
+
from PyQt6.QtCore import Qt
|
|
12
|
+
from PyQt6.QtGui import QFont
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileViewerDialog(QDialog):
|
|
17
|
+
"""
|
|
18
|
+
Dialog for viewing attached file content.
|
|
19
|
+
|
|
20
|
+
Shows:
|
|
21
|
+
- Original filename
|
|
22
|
+
- File type and size
|
|
23
|
+
- Attached date
|
|
24
|
+
- Converted markdown content (read-only)
|
|
25
|
+
- Copy to clipboard button
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, file_data: dict, parent=None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the file viewer dialog.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
file_data: Dictionary with file metadata and content
|
|
34
|
+
parent: Parent widget
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(parent)
|
|
37
|
+
|
|
38
|
+
self.file_data = file_data
|
|
39
|
+
self.setup_ui()
|
|
40
|
+
|
|
41
|
+
def setup_ui(self):
|
|
42
|
+
"""Setup the dialog UI"""
|
|
43
|
+
self.setWindowTitle("View Attached File")
|
|
44
|
+
self.setModal(True)
|
|
45
|
+
self.resize(800, 600)
|
|
46
|
+
|
|
47
|
+
layout = QVBoxLayout(self)
|
|
48
|
+
layout.setSpacing(10)
|
|
49
|
+
layout.setContentsMargins(15, 15, 15, 15)
|
|
50
|
+
|
|
51
|
+
# File info group
|
|
52
|
+
info_group = QGroupBox("File Information")
|
|
53
|
+
info_layout = QVBoxLayout(info_group)
|
|
54
|
+
|
|
55
|
+
# Original filename
|
|
56
|
+
name_label = QLabel(f"<b>Filename:</b> {self.file_data.get('original_name', 'Unknown')}")
|
|
57
|
+
name_label.setWordWrap(True)
|
|
58
|
+
info_layout.addWidget(name_label)
|
|
59
|
+
|
|
60
|
+
# File type and size
|
|
61
|
+
file_type = self.file_data.get('file_type', 'Unknown')
|
|
62
|
+
size_bytes = self.file_data.get('size_bytes', 0)
|
|
63
|
+
size_kb = size_bytes / 1024
|
|
64
|
+
|
|
65
|
+
if size_kb < 1024:
|
|
66
|
+
size_str = f"{size_kb:.1f} KB"
|
|
67
|
+
else:
|
|
68
|
+
size_str = f"{size_kb / 1024:.1f} MB"
|
|
69
|
+
|
|
70
|
+
type_size_label = QLabel(f"<b>Type:</b> {file_type} <b>Size:</b> {size_str}")
|
|
71
|
+
info_layout.addWidget(type_size_label)
|
|
72
|
+
|
|
73
|
+
# Attached date
|
|
74
|
+
attached_at = self.file_data.get('attached_at', '')
|
|
75
|
+
if attached_at:
|
|
76
|
+
try:
|
|
77
|
+
# Parse ISO format date
|
|
78
|
+
dt = datetime.fromisoformat(attached_at)
|
|
79
|
+
date_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
80
|
+
except:
|
|
81
|
+
date_str = attached_at
|
|
82
|
+
else:
|
|
83
|
+
date_str = "Unknown"
|
|
84
|
+
|
|
85
|
+
date_label = QLabel(f"<b>Attached:</b> {date_str}")
|
|
86
|
+
info_layout.addWidget(date_label)
|
|
87
|
+
|
|
88
|
+
layout.addWidget(info_group)
|
|
89
|
+
|
|
90
|
+
# Content group
|
|
91
|
+
content_group = QGroupBox("Converted Content (Markdown)")
|
|
92
|
+
content_layout = QVBoxLayout(content_group)
|
|
93
|
+
|
|
94
|
+
# Content viewer (read-only text editor)
|
|
95
|
+
self.content_viewer = QTextEdit()
|
|
96
|
+
self.content_viewer.setReadOnly(True)
|
|
97
|
+
self.content_viewer.setFont(QFont("Consolas", 9))
|
|
98
|
+
|
|
99
|
+
# Set content
|
|
100
|
+
content = self.file_data.get('content', '')
|
|
101
|
+
if content:
|
|
102
|
+
self.content_viewer.setPlainText(content)
|
|
103
|
+
else:
|
|
104
|
+
self.content_viewer.setPlainText("(No content available)")
|
|
105
|
+
|
|
106
|
+
# Move cursor to top
|
|
107
|
+
cursor = self.content_viewer.textCursor()
|
|
108
|
+
cursor.movePosition(cursor.MoveOperation.Start)
|
|
109
|
+
self.content_viewer.setTextCursor(cursor)
|
|
110
|
+
|
|
111
|
+
content_layout.addWidget(self.content_viewer)
|
|
112
|
+
|
|
113
|
+
layout.addWidget(content_group)
|
|
114
|
+
|
|
115
|
+
# Button bar
|
|
116
|
+
button_layout = QHBoxLayout()
|
|
117
|
+
button_layout.setSpacing(8)
|
|
118
|
+
|
|
119
|
+
# Copy button
|
|
120
|
+
copy_btn = QPushButton("📋 Copy to Clipboard")
|
|
121
|
+
copy_btn.setToolTip("Copy content to clipboard")
|
|
122
|
+
copy_btn.clicked.connect(self.copy_to_clipboard)
|
|
123
|
+
button_layout.addWidget(copy_btn)
|
|
124
|
+
|
|
125
|
+
button_layout.addStretch()
|
|
126
|
+
|
|
127
|
+
# Close button
|
|
128
|
+
close_btn = QPushButton("Close")
|
|
129
|
+
close_btn.setDefault(True)
|
|
130
|
+
close_btn.clicked.connect(self.accept)
|
|
131
|
+
button_layout.addWidget(close_btn)
|
|
132
|
+
|
|
133
|
+
layout.addLayout(button_layout)
|
|
134
|
+
|
|
135
|
+
def copy_to_clipboard(self):
|
|
136
|
+
"""Copy content to clipboard"""
|
|
137
|
+
content = self.file_data.get('content', '')
|
|
138
|
+
if content:
|
|
139
|
+
clipboard = QApplication.clipboard()
|
|
140
|
+
clipboard.setText(content)
|
|
141
|
+
|
|
142
|
+
QMessageBox.information(
|
|
143
|
+
self,
|
|
144
|
+
"Copied",
|
|
145
|
+
"Content copied to clipboard.",
|
|
146
|
+
QMessageBox.StandardButton.Ok
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
QMessageBox.warning(
|
|
150
|
+
self,
|
|
151
|
+
"No Content",
|
|
152
|
+
"No content available to copy.",
|
|
153
|
+
QMessageBox.StandardButton.Ok
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class FileRemoveConfirmDialog(QDialog):
|
|
158
|
+
"""
|
|
159
|
+
Confirmation dialog for removing attached files.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, filename: str, parent=None):
|
|
163
|
+
"""
|
|
164
|
+
Initialize the confirmation dialog.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
filename: Name of file to remove
|
|
168
|
+
parent: Parent widget
|
|
169
|
+
"""
|
|
170
|
+
super().__init__(parent)
|
|
171
|
+
|
|
172
|
+
self.filename = filename
|
|
173
|
+
self.setup_ui()
|
|
174
|
+
|
|
175
|
+
def setup_ui(self):
|
|
176
|
+
"""Setup the dialog UI"""
|
|
177
|
+
self.setWindowTitle("Confirm Remove File")
|
|
178
|
+
self.setModal(True)
|
|
179
|
+
|
|
180
|
+
layout = QVBoxLayout(self)
|
|
181
|
+
layout.setSpacing(15)
|
|
182
|
+
layout.setContentsMargins(20, 20, 20, 20)
|
|
183
|
+
|
|
184
|
+
# Warning icon and message
|
|
185
|
+
msg_label = QLabel(
|
|
186
|
+
f"⚠️ Are you sure you want to remove this file?\n\n"
|
|
187
|
+
f"<b>{self.filename}</b>\n\n"
|
|
188
|
+
f"The file will be permanently deleted from disk."
|
|
189
|
+
)
|
|
190
|
+
msg_label.setWordWrap(True)
|
|
191
|
+
msg_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
192
|
+
layout.addWidget(msg_label)
|
|
193
|
+
|
|
194
|
+
# Button bar
|
|
195
|
+
button_layout = QHBoxLayout()
|
|
196
|
+
button_layout.setSpacing(8)
|
|
197
|
+
|
|
198
|
+
# Cancel button
|
|
199
|
+
cancel_btn = QPushButton("Cancel")
|
|
200
|
+
cancel_btn.setDefault(True)
|
|
201
|
+
cancel_btn.clicked.connect(self.reject)
|
|
202
|
+
button_layout.addWidget(cancel_btn)
|
|
203
|
+
|
|
204
|
+
# Remove button
|
|
205
|
+
remove_btn = QPushButton("Remove File")
|
|
206
|
+
remove_btn.setStyleSheet("background-color: #d32f2f; color: white;")
|
|
207
|
+
remove_btn.clicked.connect(self.accept)
|
|
208
|
+
button_layout.addWidget(remove_btn)
|
|
209
|
+
|
|
210
|
+
layout.addLayout(button_layout)
|