televault 0.1.0__py3-none-any.whl → 2.0.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.
- televault/__init__.py +1 -1
- televault/chunker.py +29 -27
- televault/cli.py +237 -90
- televault/compress.py +59 -23
- televault/config.py +16 -17
- televault/core.py +140 -203
- televault/crypto.py +26 -33
- televault/models.py +29 -30
- televault/telegram.py +136 -107
- televault/tui.py +632 -0
- televault-2.0.0.dist-info/METADATA +310 -0
- televault-2.0.0.dist-info/RECORD +14 -0
- {televault-0.1.0.dist-info → televault-2.0.0.dist-info}/entry_points.txt +1 -0
- televault-0.1.0.dist-info/METADATA +0 -242
- televault-0.1.0.dist-info/RECORD +0 -13
- {televault-0.1.0.dist-info → televault-2.0.0.dist-info}/WHEEL +0 -0
televault/tui.py
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
"""Textual TUI for TeleVault."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
10
|
+
from textual.reactive import reactive
|
|
11
|
+
from textual.screen import Screen
|
|
12
|
+
from textual.widgets import (
|
|
13
|
+
Button,
|
|
14
|
+
DataTable,
|
|
15
|
+
Footer,
|
|
16
|
+
Header,
|
|
17
|
+
Input,
|
|
18
|
+
Label,
|
|
19
|
+
Static,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .cli import format_size
|
|
23
|
+
from .config import Config
|
|
24
|
+
from .core import TeleVault
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VaultApp(App):
|
|
30
|
+
"""Main TeleVault TUI Application."""
|
|
31
|
+
|
|
32
|
+
CSS = """
|
|
33
|
+
Screen {
|
|
34
|
+
align: center middle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#main-container {
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#sidebar {
|
|
43
|
+
width: 25;
|
|
44
|
+
height: 100%;
|
|
45
|
+
dock: left;
|
|
46
|
+
background: $surface-darken-1;
|
|
47
|
+
padding: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#content {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
padding: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.sidebar-button {
|
|
57
|
+
width: 100%;
|
|
58
|
+
margin: 1 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.stats-box {
|
|
62
|
+
height: auto;
|
|
63
|
+
padding: 1;
|
|
64
|
+
background: $surface-darken-2;
|
|
65
|
+
border: solid $primary;
|
|
66
|
+
margin: 1 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
DataTable {
|
|
70
|
+
height: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#status-bar {
|
|
74
|
+
dock: bottom;
|
|
75
|
+
height: 3;
|
|
76
|
+
background: $surface-darken-1;
|
|
77
|
+
color: $text;
|
|
78
|
+
padding: 0 2;
|
|
79
|
+
content-align: center middle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#search-input {
|
|
83
|
+
width: 100%;
|
|
84
|
+
margin: 1 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.title {
|
|
88
|
+
text-align: center;
|
|
89
|
+
text-style: bold;
|
|
90
|
+
color: $primary;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ProgressBar {
|
|
94
|
+
width: 100%;
|
|
95
|
+
margin: 1 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#login-screen {
|
|
99
|
+
align: center middle;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.login-container {
|
|
103
|
+
width: 60;
|
|
104
|
+
height: auto;
|
|
105
|
+
border: solid $primary;
|
|
106
|
+
padding: 2;
|
|
107
|
+
background: $surface;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.info-text {
|
|
111
|
+
color: $text-muted;
|
|
112
|
+
text-align: center;
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
BINDINGS = [
|
|
117
|
+
Binding("q", "quit", "Quit", show=True),
|
|
118
|
+
Binding("r", "refresh", "Refresh", show=True),
|
|
119
|
+
Binding("u", "upload", "Upload", show=True),
|
|
120
|
+
Binding("d", "download", "Download", show=True),
|
|
121
|
+
Binding("delete", "delete", "Delete", show=True),
|
|
122
|
+
Binding("s", "search", "Search", show=True),
|
|
123
|
+
Binding("l", "login", "Login", show=True),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
vault = reactive(None)
|
|
127
|
+
files = reactive([])
|
|
128
|
+
status_message = reactive("Ready")
|
|
129
|
+
is_authenticated = reactive(False)
|
|
130
|
+
|
|
131
|
+
def __init__(self):
|
|
132
|
+
super().__init__()
|
|
133
|
+
self.vault_instance = None
|
|
134
|
+
self.config = Config.load_or_create()
|
|
135
|
+
self.selected_file = None
|
|
136
|
+
|
|
137
|
+
def compose(self) -> ComposeResult:
|
|
138
|
+
"""Compose the main UI."""
|
|
139
|
+
yield Header(show_clock=True)
|
|
140
|
+
|
|
141
|
+
if not self.is_authenticated:
|
|
142
|
+
yield from self._compose_login_screen()
|
|
143
|
+
else:
|
|
144
|
+
yield from self._compose_main_screen()
|
|
145
|
+
|
|
146
|
+
yield Footer()
|
|
147
|
+
|
|
148
|
+
def _compose_login_screen(self) -> ComposeResult:
|
|
149
|
+
"""Compose the login screen."""
|
|
150
|
+
with Container(id="login-screen"), Container(classes="login-container"):
|
|
151
|
+
yield Label("TeleVault", classes="title")
|
|
152
|
+
yield Label("")
|
|
153
|
+
yield Label("Welcome to TeleVault", classes="info-text")
|
|
154
|
+
yield Label("Your encrypted Telegram cloud storage", classes="info-text")
|
|
155
|
+
yield Label("")
|
|
156
|
+
|
|
157
|
+
yield Label("Status: Not Authenticated", id="auth-status")
|
|
158
|
+
yield Label("")
|
|
159
|
+
|
|
160
|
+
with Horizontal():
|
|
161
|
+
yield Button("🔐 Login", id="btn-login", variant="primary")
|
|
162
|
+
yield Button("❌ Exit", id="btn-exit", variant="error")
|
|
163
|
+
|
|
164
|
+
yield Label("")
|
|
165
|
+
yield Label("Press Ctrl+C to exit anytime", classes="info-text")
|
|
166
|
+
|
|
167
|
+
def _compose_main_screen(self) -> ComposeResult:
|
|
168
|
+
"""Compose the main application screen."""
|
|
169
|
+
with Container(id="main-container"):
|
|
170
|
+
# Sidebar
|
|
171
|
+
with Vertical(id="sidebar"):
|
|
172
|
+
yield Label("📁 TeleVault", classes="title")
|
|
173
|
+
yield Label("")
|
|
174
|
+
|
|
175
|
+
# Stats
|
|
176
|
+
with Container(classes="stats-box"):
|
|
177
|
+
yield Label("📊 Statistics", classes="title")
|
|
178
|
+
yield Label("Files: 0", id="stat-files")
|
|
179
|
+
yield Label("Total Size: 0 B", id="stat-size")
|
|
180
|
+
yield Label("Storage: -", id="stat-storage")
|
|
181
|
+
|
|
182
|
+
yield Label("")
|
|
183
|
+
yield Button("📤 Upload", id="btn-upload", classes="sidebar-button")
|
|
184
|
+
yield Button("🔍 Search", id="btn-search", classes="sidebar-button")
|
|
185
|
+
yield Button("🔄 Refresh", id="btn-refresh", classes="sidebar-button")
|
|
186
|
+
yield Button("ℹ️ Status", id="btn-status", classes="sidebar-button")
|
|
187
|
+
yield Button("👤 Whoami", id="btn-whoami", classes="sidebar-button")
|
|
188
|
+
yield Button("🔓 Logout", id="btn-logout", classes="sidebar-button")
|
|
189
|
+
|
|
190
|
+
# Main content
|
|
191
|
+
with Vertical(id="content"):
|
|
192
|
+
yield Label("📁 File Browser", classes="title")
|
|
193
|
+
yield Input(placeholder="Search files...", id="search-input")
|
|
194
|
+
|
|
195
|
+
# File table
|
|
196
|
+
table = DataTable(id="file-table")
|
|
197
|
+
table.add_columns("ID", "Name", "Size", "Chunks", "Encrypted", "Actions")
|
|
198
|
+
table.cursor_type = "row"
|
|
199
|
+
table.zebra_stripes = True
|
|
200
|
+
yield table
|
|
201
|
+
|
|
202
|
+
# Status bar
|
|
203
|
+
yield Static(
|
|
204
|
+
"Ready - Press 'q' to quit, 'u' to upload, 'd' to download", id="status-bar"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def on_mount(self) -> None:
|
|
208
|
+
"""Handle app mount."""
|
|
209
|
+
self.title = "TeleVault - Encrypted Cloud Storage"
|
|
210
|
+
|
|
211
|
+
# Check authentication on mount
|
|
212
|
+
await self._check_auth()
|
|
213
|
+
|
|
214
|
+
async def _check_auth(self) -> None:
|
|
215
|
+
"""Check if user is authenticated."""
|
|
216
|
+
try:
|
|
217
|
+
self.vault_instance = TeleVault()
|
|
218
|
+
await self.vault_instance.connect(skip_channel=True)
|
|
219
|
+
|
|
220
|
+
if await self.vault_instance.is_authenticated():
|
|
221
|
+
self.is_authenticated = True
|
|
222
|
+
await self.vault_instance.disconnect()
|
|
223
|
+
self.refresh(layout=True)
|
|
224
|
+
await self._load_files()
|
|
225
|
+
else:
|
|
226
|
+
await self.vault_instance.disconnect()
|
|
227
|
+
self.is_authenticated = False
|
|
228
|
+
self.status_message = "Not logged in - Press 'l' to login"
|
|
229
|
+
except Exception as e:
|
|
230
|
+
self.status_message = f"Error: {str(e)}"
|
|
231
|
+
self.is_authenticated = False
|
|
232
|
+
|
|
233
|
+
async def _load_files(self) -> None:
|
|
234
|
+
"""Load files from vault."""
|
|
235
|
+
if not self.is_authenticated:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
self.status_message = "Loading files..."
|
|
240
|
+
vault = TeleVault()
|
|
241
|
+
await vault.connect()
|
|
242
|
+
|
|
243
|
+
files = await vault.list_files()
|
|
244
|
+
self.files = files
|
|
245
|
+
|
|
246
|
+
# Update table
|
|
247
|
+
table = self.query_one("#file-table", DataTable)
|
|
248
|
+
table.clear()
|
|
249
|
+
|
|
250
|
+
for f in files:
|
|
251
|
+
table.add_row(
|
|
252
|
+
f.id[:8],
|
|
253
|
+
f.name[:40] + "..." if len(f.name) > 40 else f.name,
|
|
254
|
+
format_size(f.size),
|
|
255
|
+
str(f.chunk_count),
|
|
256
|
+
"🔒" if f.encrypted else "📄",
|
|
257
|
+
"[Enter] Download | [Del] Delete",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Update stats
|
|
261
|
+
total_size = sum(f.size for f in files)
|
|
262
|
+
self.query_one("#stat-files", Label).update(f"Files: {len(files)}")
|
|
263
|
+
self.query_one("#stat-size", Label).update(f"Total Size: {format_size(total_size)}")
|
|
264
|
+
|
|
265
|
+
await vault.disconnect()
|
|
266
|
+
self.status_message = f"Loaded {len(files)} files"
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
self.status_message = f"Error loading files: {str(e)}"
|
|
270
|
+
|
|
271
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
272
|
+
"""Handle button presses."""
|
|
273
|
+
button_id = event.button.id
|
|
274
|
+
|
|
275
|
+
if button_id == "btn-login":
|
|
276
|
+
await self._do_login()
|
|
277
|
+
elif button_id == "btn-exit":
|
|
278
|
+
self.exit()
|
|
279
|
+
elif button_id == "btn-upload":
|
|
280
|
+
await self._do_upload()
|
|
281
|
+
elif button_id == "btn-search":
|
|
282
|
+
await self._do_search()
|
|
283
|
+
elif button_id == "btn-refresh":
|
|
284
|
+
await self._load_files()
|
|
285
|
+
elif button_id == "btn-status":
|
|
286
|
+
await self._show_status()
|
|
287
|
+
elif button_id == "btn-whoami":
|
|
288
|
+
await self._show_whoami()
|
|
289
|
+
elif button_id == "btn-logout":
|
|
290
|
+
await self._do_logout()
|
|
291
|
+
|
|
292
|
+
async def _do_login(self) -> None:
|
|
293
|
+
"""Show login dialog."""
|
|
294
|
+
self.push_screen(LoginScreen())
|
|
295
|
+
|
|
296
|
+
async def _do_upload(self) -> None:
|
|
297
|
+
"""Show upload dialog."""
|
|
298
|
+
if not self.is_authenticated:
|
|
299
|
+
self.notify("Please login first", severity="error")
|
|
300
|
+
return
|
|
301
|
+
self.push_screen(UploadScreen())
|
|
302
|
+
|
|
303
|
+
async def _do_search(self) -> None:
|
|
304
|
+
"""Focus search input."""
|
|
305
|
+
if not self.is_authenticated:
|
|
306
|
+
self.notify("Please login first", severity="error")
|
|
307
|
+
return
|
|
308
|
+
self.query_one("#search-input", Input).focus()
|
|
309
|
+
|
|
310
|
+
async def _show_status(self) -> None:
|
|
311
|
+
"""Show vault status."""
|
|
312
|
+
if not self.is_authenticated:
|
|
313
|
+
self.notify("Please login first", severity="error")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
vault = TeleVault()
|
|
318
|
+
await vault.connect()
|
|
319
|
+
status = await vault.get_status()
|
|
320
|
+
await vault.disconnect()
|
|
321
|
+
|
|
322
|
+
message = f"""
|
|
323
|
+
📊 Vault Status
|
|
324
|
+
|
|
325
|
+
Channel ID: {status["channel_id"]}
|
|
326
|
+
Files: {status["file_count"]}
|
|
327
|
+
Total Size: {format_size(status["total_size"])}
|
|
328
|
+
Stored Size: {format_size(status["stored_size"])}
|
|
329
|
+
Compression: {status["compression_ratio"]:.1%}
|
|
330
|
+
""".strip()
|
|
331
|
+
|
|
332
|
+
self.notify(message, title="Status", timeout=10)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
self.notify(f"Error: {str(e)}", severity="error")
|
|
335
|
+
|
|
336
|
+
async def _show_whoami(self) -> None:
|
|
337
|
+
"""Show current user info."""
|
|
338
|
+
if not self.is_authenticated:
|
|
339
|
+
self.notify("Please login first", severity="error")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
vault = TeleVault()
|
|
344
|
+
await vault.connect()
|
|
345
|
+
me = await vault.telegram._client.get_me()
|
|
346
|
+
await vault.disconnect()
|
|
347
|
+
|
|
348
|
+
if me:
|
|
349
|
+
name = f"{me.first_name or ''} {me.last_name or ''}".strip()
|
|
350
|
+
username = f"@{me.username}" if me.username else "No username"
|
|
351
|
+
|
|
352
|
+
message = f"""
|
|
353
|
+
👤 Account Info
|
|
354
|
+
|
|
355
|
+
Name: {name}
|
|
356
|
+
Username: {username}
|
|
357
|
+
ID: {me.id}
|
|
358
|
+
""".strip()
|
|
359
|
+
|
|
360
|
+
self.notify(message, title="Whoami", timeout=10)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
self.notify(f"Error: {str(e)}", severity="error")
|
|
363
|
+
|
|
364
|
+
async def _do_logout(self) -> None:
|
|
365
|
+
"""Logout user."""
|
|
366
|
+
config_dir = Path.home() / ".config" / "televault"
|
|
367
|
+
telegram_config = config_dir / "telegram.json"
|
|
368
|
+
|
|
369
|
+
if telegram_config.exists():
|
|
370
|
+
telegram_config.unlink()
|
|
371
|
+
self.is_authenticated = False
|
|
372
|
+
self.files = []
|
|
373
|
+
self.refresh(layout=True)
|
|
374
|
+
self.notify("Logged out successfully", severity="information")
|
|
375
|
+
else:
|
|
376
|
+
self.notify("Not logged in", severity="warning")
|
|
377
|
+
|
|
378
|
+
def action_refresh(self) -> None:
|
|
379
|
+
"""Refresh action."""
|
|
380
|
+
asyncio.create_task(self._load_files())
|
|
381
|
+
|
|
382
|
+
def action_upload(self) -> None:
|
|
383
|
+
"""Upload action."""
|
|
384
|
+
asyncio.create_task(self._do_upload())
|
|
385
|
+
|
|
386
|
+
def action_download(self) -> None:
|
|
387
|
+
"""Download action."""
|
|
388
|
+
self.notify("Select a file and press Enter to download", severity="information")
|
|
389
|
+
|
|
390
|
+
def action_delete(self) -> None:
|
|
391
|
+
"""Delete action."""
|
|
392
|
+
self.notify("Select a file and press Delete to remove", severity="information")
|
|
393
|
+
|
|
394
|
+
def action_search(self) -> None:
|
|
395
|
+
"""Search action."""
|
|
396
|
+
asyncio.create_task(self._do_search())
|
|
397
|
+
|
|
398
|
+
def action_login(self) -> None:
|
|
399
|
+
"""Login action."""
|
|
400
|
+
asyncio.create_task(self._do_login())
|
|
401
|
+
|
|
402
|
+
async def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
403
|
+
"""Handle file selection."""
|
|
404
|
+
if not self.files:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
row_index = event.cursor_row
|
|
408
|
+
if 0 <= row_index < len(self.files):
|
|
409
|
+
self.selected_file = self.files[row_index]
|
|
410
|
+
await self._download_file(self.selected_file)
|
|
411
|
+
|
|
412
|
+
async def _download_file(self, file_metadata) -> None:
|
|
413
|
+
"""Download selected file."""
|
|
414
|
+
self.push_screen(DownloadScreen(file_metadata))
|
|
415
|
+
|
|
416
|
+
def watch_status_message(self, message: str) -> None:
|
|
417
|
+
"""Update status bar when message changes."""
|
|
418
|
+
try:
|
|
419
|
+
status_bar = self.query_one("#status-bar", Static)
|
|
420
|
+
status_bar.update(message)
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class LoginScreen(Screen):
|
|
426
|
+
"""Login screen for authentication."""
|
|
427
|
+
|
|
428
|
+
def compose(self) -> ComposeResult:
|
|
429
|
+
with Container(classes="login-container"):
|
|
430
|
+
yield Label("🔐 Login to Telegram", classes="title")
|
|
431
|
+
yield Label("")
|
|
432
|
+
yield Label("Enter your phone number:", classes="info-text")
|
|
433
|
+
yield Input(placeholder="+1234567890", id="phone-input")
|
|
434
|
+
yield Label("")
|
|
435
|
+
yield Button("Send Code", id="btn-send-code", variant="primary")
|
|
436
|
+
yield Button("Cancel", id="btn-cancel")
|
|
437
|
+
|
|
438
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
439
|
+
if event.button.id == "btn-send-code":
|
|
440
|
+
phone = self.query_one("#phone-input", Input).value
|
|
441
|
+
if phone:
|
|
442
|
+
await self._do_login(phone)
|
|
443
|
+
elif event.button.id == "btn-cancel":
|
|
444
|
+
self.app.pop_screen()
|
|
445
|
+
|
|
446
|
+
async def _do_login(self, phone: str) -> None:
|
|
447
|
+
"""Perform login."""
|
|
448
|
+
try:
|
|
449
|
+
self.app.notify("Connecting to Telegram...")
|
|
450
|
+
|
|
451
|
+
vault = TeleVault()
|
|
452
|
+
await vault.connect(skip_channel=True)
|
|
453
|
+
|
|
454
|
+
if await vault.is_authenticated():
|
|
455
|
+
self.app.notify("Already logged in!")
|
|
456
|
+
self.app.is_authenticated = True
|
|
457
|
+
self.app.refresh(layout=True)
|
|
458
|
+
await vault.disconnect()
|
|
459
|
+
self.app.pop_screen()
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
# Show code input screen
|
|
463
|
+
self.app.push_screen(CodeScreen(vault, phone))
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
self.app.notify(f"Login error: {str(e)}", severity="error")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class CodeScreen(Screen):
|
|
470
|
+
"""Screen for entering verification code."""
|
|
471
|
+
|
|
472
|
+
def __init__(self, vault: TeleVault, phone: str):
|
|
473
|
+
super().__init__()
|
|
474
|
+
self.vault = vault
|
|
475
|
+
self.phone = phone
|
|
476
|
+
|
|
477
|
+
def compose(self) -> ComposeResult:
|
|
478
|
+
with Container(classes="login-container"):
|
|
479
|
+
yield Label("📱 Verification Code", classes="title")
|
|
480
|
+
yield Label("")
|
|
481
|
+
yield Label(f"Enter the code sent to {self.phone}:", classes="info-text")
|
|
482
|
+
yield Input(placeholder="12345", id="code-input")
|
|
483
|
+
yield Label("")
|
|
484
|
+
yield Button("Verify", id="btn-verify", variant="primary")
|
|
485
|
+
yield Button("Back", id="btn-back")
|
|
486
|
+
|
|
487
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
488
|
+
if event.button.id == "btn-verify":
|
|
489
|
+
code = self.query_one("#code-input", Input).value
|
|
490
|
+
if code:
|
|
491
|
+
await self._verify_code(code)
|
|
492
|
+
elif event.button.id == "btn-back":
|
|
493
|
+
await self.vault.disconnect()
|
|
494
|
+
self.app.pop_screen()
|
|
495
|
+
|
|
496
|
+
async def _verify_code(self, code: str) -> None:
|
|
497
|
+
"""Verify the code."""
|
|
498
|
+
try:
|
|
499
|
+
await self.vault.telegram._client.sign_in(self.phone, code)
|
|
500
|
+
|
|
501
|
+
# Save session
|
|
502
|
+
session_string = self.vault.telegram._client.session.save()
|
|
503
|
+
from .telegram import TelegramConfig
|
|
504
|
+
|
|
505
|
+
config = TelegramConfig.from_env()
|
|
506
|
+
config.session_string = session_string
|
|
507
|
+
config.save()
|
|
508
|
+
|
|
509
|
+
self.app.notify("✓ Login successful!")
|
|
510
|
+
self.app.is_authenticated = True
|
|
511
|
+
self.app.refresh(layout=True)
|
|
512
|
+
await self.vault.disconnect()
|
|
513
|
+
self.app.pop_screen()
|
|
514
|
+
self.app.pop_screen() # Pop login screen too
|
|
515
|
+
|
|
516
|
+
except Exception as e:
|
|
517
|
+
self.app.notify(f"Verification failed: {str(e)}", severity="error")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class UploadScreen(Screen):
|
|
521
|
+
"""Screen for uploading files."""
|
|
522
|
+
|
|
523
|
+
def compose(self) -> ComposeResult:
|
|
524
|
+
with Container(classes="login-container"):
|
|
525
|
+
yield Label("📤 Upload File", classes="title")
|
|
526
|
+
yield Label("")
|
|
527
|
+
yield Label("Enter file path:", classes="info-text")
|
|
528
|
+
yield Input(placeholder="/path/to/file", id="path-input")
|
|
529
|
+
yield Label("")
|
|
530
|
+
yield Label("Password (optional):", classes="info-text")
|
|
531
|
+
yield Input(
|
|
532
|
+
placeholder="Leave empty to use env var", id="password-input", password=True
|
|
533
|
+
)
|
|
534
|
+
yield Label("")
|
|
535
|
+
with Horizontal():
|
|
536
|
+
yield Button("Upload", id="btn-do-upload", variant="primary")
|
|
537
|
+
yield Button("Cancel", id="btn-cancel")
|
|
538
|
+
|
|
539
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
540
|
+
if event.button.id == "btn-do-upload":
|
|
541
|
+
path = self.query_one("#path-input", Input).value
|
|
542
|
+
password = self.query_one("#password-input", Input).value
|
|
543
|
+
if path:
|
|
544
|
+
await self._upload_file(path, password or None)
|
|
545
|
+
elif event.button.id == "btn-cancel":
|
|
546
|
+
self.app.pop_screen()
|
|
547
|
+
|
|
548
|
+
async def _upload_file(self, path: str, password: str | None) -> None:
|
|
549
|
+
"""Upload the file."""
|
|
550
|
+
try:
|
|
551
|
+
self.app.notify(f"Uploading {Path(path).name}...")
|
|
552
|
+
|
|
553
|
+
vault = TeleVault(password=password)
|
|
554
|
+
await vault.connect()
|
|
555
|
+
|
|
556
|
+
metadata = await vault.upload(path)
|
|
557
|
+
await vault.disconnect()
|
|
558
|
+
|
|
559
|
+
self.app.notify(f"✓ Uploaded: {metadata.name}")
|
|
560
|
+
self.app.pop_screen()
|
|
561
|
+
|
|
562
|
+
# Refresh file list
|
|
563
|
+
await self.app._load_files()
|
|
564
|
+
|
|
565
|
+
except Exception as e:
|
|
566
|
+
self.app.notify(f"Upload failed: {str(e)}", severity="error")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class DownloadScreen(Screen):
|
|
570
|
+
"""Screen for downloading files."""
|
|
571
|
+
|
|
572
|
+
def __init__(self, file_metadata):
|
|
573
|
+
super().__init__()
|
|
574
|
+
self.file_metadata = file_metadata
|
|
575
|
+
|
|
576
|
+
def compose(self) -> ComposeResult:
|
|
577
|
+
with Container(classes="login-container"):
|
|
578
|
+
yield Label("📥 Download File", classes="title")
|
|
579
|
+
yield Label("")
|
|
580
|
+
yield Label(f"File: {self.file_metadata.name}", classes="info-text")
|
|
581
|
+
yield Label(f"Size: {format_size(self.file_metadata.size)}")
|
|
582
|
+
yield Label("")
|
|
583
|
+
yield Label("Output path (optional):", classes="info-text")
|
|
584
|
+
yield Input(placeholder="Current directory", id="output-input")
|
|
585
|
+
yield Label("")
|
|
586
|
+
if self.file_metadata.encrypted:
|
|
587
|
+
yield Label("Password:", classes="info-text")
|
|
588
|
+
yield Input(
|
|
589
|
+
placeholder="Enter decryption password", id="password-input", password=True
|
|
590
|
+
)
|
|
591
|
+
yield Label("")
|
|
592
|
+
with Horizontal():
|
|
593
|
+
yield Button("Download", id="btn-do-download", variant="primary")
|
|
594
|
+
yield Button("Cancel", id="btn-cancel")
|
|
595
|
+
|
|
596
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
597
|
+
if event.button.id == "btn-do-download":
|
|
598
|
+
output = self.query_one("#output-input", Input).value
|
|
599
|
+
password_input = (
|
|
600
|
+
self.query_one("#password-input", Input) if self.file_metadata.encrypted else None
|
|
601
|
+
)
|
|
602
|
+
password = password_input.value if password_input else None
|
|
603
|
+
await self._download_file(output or None, password or None)
|
|
604
|
+
elif event.button.id == "btn-cancel":
|
|
605
|
+
self.app.pop_screen()
|
|
606
|
+
|
|
607
|
+
async def _download_file(self, output: str | None, password: str | None) -> None:
|
|
608
|
+
"""Download the file."""
|
|
609
|
+
try:
|
|
610
|
+
self.app.notify(f"Downloading {self.file_metadata.name}...")
|
|
611
|
+
|
|
612
|
+
vault = TeleVault(password=password)
|
|
613
|
+
await vault.connect()
|
|
614
|
+
|
|
615
|
+
output_path = await vault.download(self.file_metadata.id, output_path=output)
|
|
616
|
+
await vault.disconnect()
|
|
617
|
+
|
|
618
|
+
self.app.notify(f"✓ Downloaded to: {output_path}")
|
|
619
|
+
self.app.pop_screen()
|
|
620
|
+
|
|
621
|
+
except Exception as e:
|
|
622
|
+
self.app.notify(f"Download failed: {str(e)}", severity="error")
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def run_tui():
|
|
626
|
+
"""Run the TUI application."""
|
|
627
|
+
app = VaultApp()
|
|
628
|
+
app.run()
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
if __name__ == "__main__":
|
|
632
|
+
run_tui()
|