data-sourcerer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. data_sourcerer-0.1.0.dist-info/METADATA +52 -0
  2. data_sourcerer-0.1.0.dist-info/RECORD +95 -0
  3. data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
  4. data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
  5. data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
  7. sourcerer/__init__.py +15 -0
  8. sourcerer/domain/__init__.py +13 -0
  9. sourcerer/domain/access_credentials/__init__.py +7 -0
  10. sourcerer/domain/access_credentials/entities.py +53 -0
  11. sourcerer/domain/access_credentials/exceptions.py +17 -0
  12. sourcerer/domain/access_credentials/repositories.py +84 -0
  13. sourcerer/domain/access_credentials/services.py +91 -0
  14. sourcerer/domain/file_system/__init__.py +7 -0
  15. sourcerer/domain/file_system/entities.py +70 -0
  16. sourcerer/domain/file_system/exceptions.py +17 -0
  17. sourcerer/domain/file_system/services.py +64 -0
  18. sourcerer/domain/shared/__init__.py +6 -0
  19. sourcerer/domain/shared/entities.py +18 -0
  20. sourcerer/domain/storage_provider/__init__.py +7 -0
  21. sourcerer/domain/storage_provider/entities.py +86 -0
  22. sourcerer/domain/storage_provider/exceptions.py +17 -0
  23. sourcerer/domain/storage_provider/services.py +130 -0
  24. sourcerer/infrastructure/__init__.py +13 -0
  25. sourcerer/infrastructure/access_credentials/__init__.py +7 -0
  26. sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
  27. sourcerer/infrastructure/access_credentials/registry.py +120 -0
  28. sourcerer/infrastructure/access_credentials/repositories.py +119 -0
  29. sourcerer/infrastructure/access_credentials/services.py +396 -0
  30. sourcerer/infrastructure/db/__init__.py +6 -0
  31. sourcerer/infrastructure/db/config.py +73 -0
  32. sourcerer/infrastructure/db/models.py +47 -0
  33. sourcerer/infrastructure/file_system/__init__.py +7 -0
  34. sourcerer/infrastructure/file_system/exceptions.py +89 -0
  35. sourcerer/infrastructure/file_system/services.py +147 -0
  36. sourcerer/infrastructure/storage_provider/__init__.py +7 -0
  37. sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
  38. sourcerer/infrastructure/storage_provider/registry.py +84 -0
  39. sourcerer/infrastructure/storage_provider/services.py +509 -0
  40. sourcerer/infrastructure/utils.py +106 -0
  41. sourcerer/presentation/__init__.py +12 -0
  42. sourcerer/presentation/app.py +36 -0
  43. sourcerer/presentation/di_container.py +46 -0
  44. sourcerer/presentation/screens/__init__.py +0 -0
  45. sourcerer/presentation/screens/critical_error/__init__.py +0 -0
  46. sourcerer/presentation/screens/critical_error/main.py +78 -0
  47. sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
  48. sourcerer/presentation/screens/file_system_finder/main.py +248 -0
  49. sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
  50. sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
  51. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
  52. sourcerer/presentation/screens/main/__init__.py +0 -0
  53. sourcerer/presentation/screens/main/main.py +469 -0
  54. sourcerer/presentation/screens/main/messages/__init__.py +0 -0
  55. sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
  56. sourcerer/presentation/screens/main/messages/download_request.py +12 -0
  57. sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
  58. sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
  59. sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
  60. sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
  61. sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
  62. sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
  63. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
  64. sourcerer/presentation/screens/main/styles.tcss +32 -0
  65. sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
  66. sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
  67. sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
  68. sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
  69. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
  70. sourcerer/presentation/screens/preview_content/__init__.py +0 -0
  71. sourcerer/presentation/screens/preview_content/main.py +59 -0
  72. sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
  73. sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
  74. sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
  75. sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
  76. sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
  77. sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
  78. sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
  79. sourcerer/presentation/screens/question/__init__.py +0 -0
  80. sourcerer/presentation/screens/question/main.py +31 -0
  81. sourcerer/presentation/screens/question/styles.tcss +33 -0
  82. sourcerer/presentation/screens/shared/__init__.py +0 -0
  83. sourcerer/presentation/screens/shared/containers.py +13 -0
  84. sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
  85. sourcerer/presentation/screens/shared/widgets/button.py +54 -0
  86. sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
  87. sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
  88. sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
  89. sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
  90. sourcerer/presentation/settings.py +31 -0
  91. sourcerer/presentation/themes/__init__.py +0 -0
  92. sourcerer/presentation/themes/github_dark.py +21 -0
  93. sourcerer/presentation/utils.py +69 -0
  94. sourcerer/settings.py +72 -0
  95. sourcerer/utils.py +32 -0
@@ -0,0 +1,691 @@
1
+ """Storage content display widgets for the Sourcerer application.
2
+
3
+ This module provides widgets for displaying and interacting with storage content,
4
+ including files and folders. It handles file selection, navigation, and content
5
+ display with search functionality.
6
+ """
7
+
8
+ import os.path
9
+ from dataclasses import dataclass
10
+ from enum import Enum, auto
11
+
12
+ from textual import events, on
13
+ from textual.app import ComposeResult
14
+ from textual.containers import (
15
+ VerticalScroll,
16
+ Horizontal,
17
+ Center,
18
+ Middle,
19
+ Container,
20
+ Vertical,
21
+ )
22
+ from textual.css.query import NoMatches
23
+ from textual.message import Message
24
+ from textual.reactive import reactive
25
+ from textual.widgets import Label, Static, Input, Checkbox
26
+
27
+ from sourcerer.domain.storage_provider.entities import StorageContent
28
+ from sourcerer.presentation.screens.main.messages.delete_request import DeleteRequest
29
+ from sourcerer.presentation.screens.main.messages.download_request import (
30
+ DownloadRequest,
31
+ )
32
+ from sourcerer.presentation.screens.main.messages.preview_request import PreviewRequest
33
+ from sourcerer.presentation.screens.main.messages.select_storage_item import (
34
+ SelectStorageItem,
35
+ )
36
+ from sourcerer.presentation.screens.main.messages.uncheck_files_request import (
37
+ UncheckFilesRequest,
38
+ )
39
+ from sourcerer.presentation.screens.main.messages.upload_request import UploadRequest
40
+ from sourcerer.presentation.screens.shared.widgets.button import Button
41
+ from sourcerer.presentation.settings import NO_DATA_LOGO
42
+ from sourcerer.settings import (
43
+ UPLOAD_ICON,
44
+ DOWNLOAD_ICON,
45
+ PREVIEW_ICON,
46
+ DIRECTORY_ICON,
47
+ FILE_ICON,
48
+ )
49
+
50
+
51
+ class ActionType(Enum):
52
+ """
53
+ Enum representing the different types of actions that can be performed on storage items.
54
+
55
+ This enum is used to replace string literals for action types, providing better type safety,
56
+ code completion, and making the code more maintainable.
57
+ """
58
+
59
+ UPLOAD = auto()
60
+ DELETE = auto()
61
+ DOWNLOAD = auto()
62
+ UNCHECK_ALL = auto()
63
+ PREVIEW = auto()
64
+
65
+ @classmethod
66
+ def from_string(cls, action_str: str) -> "ActionType":
67
+ """
68
+ Convert a string action name to the corresponding enum value.
69
+
70
+ Args:
71
+ action_str (str): The string representation of the action
72
+
73
+ Returns:
74
+ ActionType: The corresponding enum value
75
+
76
+ Raises:
77
+ ValueError: If the string doesn't match any known action type
78
+ """
79
+ action_map = {
80
+ "upload": cls.UPLOAD,
81
+ "delete": cls.DELETE,
82
+ "download": cls.DOWNLOAD,
83
+ "uncheck_all": cls.UNCHECK_ALL,
84
+ "preview": cls.PREVIEW,
85
+ }
86
+
87
+ if action_str not in action_map:
88
+ raise ValueError(f"Unknown action type: {action_str}")
89
+
90
+ return action_map[action_str]
91
+
92
+
93
+ class FileMetaLabel(Static):
94
+ """Widget for displaying file metadata information.
95
+
96
+ This widget is used to show file metadata such as size, modification date,
97
+ or other file properties in a transparent background.
98
+ """
99
+
100
+ DEFAULT_CSS = """
101
+ FileMetaLabel {
102
+ margin: 0 0;
103
+ padding: 0 0;
104
+ background: transparent;
105
+ }
106
+ """
107
+ can_focus = False
108
+
109
+
110
+ class PathSelector(Label):
111
+ """Widget for displaying and selecting storage paths.
112
+
113
+ This widget shows the current path in the storage and allows navigation
114
+ by clicking on path segments.
115
+
116
+ Args:
117
+ storage: The name of the storage provider
118
+ path: The current path in the storage
119
+ access_credentials_uuid: UUID of the access credentials being used
120
+ """
121
+
122
+ def __init__(self, storage, path, access_credentials_uuid, *args, **kwargs):
123
+ super().__init__(*args, **kwargs)
124
+ self.storage = storage
125
+ self.path = path
126
+ self.access_credentials_uuid = access_credentials_uuid
127
+
128
+ def on_click(self, _: events.Click) -> None:
129
+ """Handle click events to navigate to the selected path."""
130
+ self.post_message(
131
+ SelectStorageItem(self.storage, self.path, self.access_credentials_uuid)
132
+ )
133
+
134
+
135
+ class FolderItem(Horizontal):
136
+ """Widget for displaying and interacting with folder items.
137
+
138
+ This widget represents a folder in the storage content view, allowing
139
+ navigation into the folder and visual feedback on hover/selection.
140
+ """
141
+
142
+ DEFAULT_CSS = """
143
+ FolderItem {
144
+ margin-bottom: 1;
145
+ }
146
+ FolderItem.active {
147
+ background: $secondary;
148
+ color: $panel;
149
+ }
150
+ """
151
+ can_focus = False
152
+
153
+ def __init__(
154
+ self, storage, access_credentials_uuid, parent_path, folder, *args, **kwargs
155
+ ):
156
+ """Initialize a folder item widget.
157
+
158
+ Args:
159
+ storage: The name of the storage provider
160
+ access_credentials_uuid: UUID of the access credentials being used
161
+ parent_path: The parent path of the folder
162
+ folder: The folder name
163
+ """
164
+ super().__init__(*args, **kwargs)
165
+ self.storage = storage
166
+ self.access_credentials_uuid = access_credentials_uuid
167
+ self.parent_path = parent_path
168
+ self.folder = folder
169
+
170
+ def compose(self):
171
+ """Compose the folder item layout with folder name and icon."""
172
+ yield Label(f"{DIRECTORY_ICON}{self.folder.key}")
173
+
174
+ def on_click(self, _: events.Click) -> None:
175
+ """Handle click events to navigate into the folder."""
176
+ path = self.folder.key
177
+ if self.parent_path:
178
+ path = self.parent_path.strip("/") + "/" + path
179
+
180
+ self.post_message(
181
+ SelectStorageItem(self.storage, path, self.access_credentials_uuid)
182
+ )
183
+
184
+ def on_mouse_move(self, _) -> None:
185
+ """Handle mouse move events to highlight the folder."""
186
+ self.add_class("active")
187
+
188
+ def on_leave(self, _) -> None:
189
+ """Handle mouse leave events to remove highlight."""
190
+ self.remove_class("active")
191
+
192
+
193
+ class FileItem(Horizontal):
194
+ """Widget for displaying and interacting with file items.
195
+
196
+ This widget represents a file in the storage content view, allowing
197
+ selection and visual feedback on hover/selection.
198
+ """
199
+
200
+ DEFAULT_CSS = """
201
+ FileItem {
202
+ margin-bottom: 1;
203
+ }
204
+ FileItem.active {
205
+ background: $secondary;
206
+ color: $panel;
207
+ }
208
+ Checkbox {
209
+ border: none;
210
+ padding: 0 0;
211
+ display: none;
212
+ &:focus {
213
+ border: none;
214
+ background-tint: $foreground 5%;
215
+ }
216
+ }
217
+ """
218
+ can_focus = False
219
+
220
+ @dataclass
221
+ class Selected(Message):
222
+ """Message sent when a file is selected."""
223
+
224
+ name: str
225
+
226
+ @dataclass
227
+ class Preview(Message):
228
+ """Message sent when a file preview is selected."""
229
+
230
+ name: str
231
+
232
+ @dataclass
233
+ class Unselect(Message):
234
+ """Message sent when a file is unselected."""
235
+
236
+ name: str
237
+
238
+ def on_mount(self):
239
+ """Initialize the file item on mount."""
240
+ self.add_class("file-item")
241
+
242
+ def __init__(self, storage, parent_path, file, *args, **kwargs):
243
+ """Initialize a file item widget.
244
+
245
+ Args:
246
+ storage: The name of the storage provider
247
+ parent_path: The parent path of the file
248
+ file: The file name
249
+ """
250
+ super().__init__(*args, **kwargs)
251
+ self.storage = storage
252
+ self.parent_path = parent_path
253
+ self.file = file
254
+
255
+ def compose(self):
256
+ yield Checkbox()
257
+ yield FileMetaLabel(f"{FILE_ICON} {self.file.key}", classes="file_name")
258
+ yield FileMetaLabel(
259
+ f"[$primary]{self.file.size}[/$primary]", classes="file_size"
260
+ )
261
+ yield FileMetaLabel(str(self.file.date_modified), classes="file_date")
262
+ if self.file.is_text:
263
+ yield Button(f"{PREVIEW_ICON}", name="preview", classes="download")
264
+
265
+ def on_mouse_move(self, _) -> None:
266
+ """Handle mouse move events to highlight the file."""
267
+ self.add_class("active")
268
+
269
+ def on_leave(self, _) -> None:
270
+ """Handle mouse leave events to remove highlight."""
271
+ self.remove_class("active")
272
+
273
+ def on_click(self, event: events.Click) -> None:
274
+ """Handle click events to toggle file selection."""
275
+ preview_button = None
276
+ try:
277
+ preview_button = self.query_one(Button)
278
+ except NoMatches:
279
+ pass
280
+ if event.widget is preview_button:
281
+ self.post_message(self.Preview(self.file.key))
282
+ return
283
+
284
+ checkbox = self.query_one(Checkbox)
285
+ if event.widget is not checkbox:
286
+ checkbox.value = not checkbox.value
287
+ if checkbox.value:
288
+ self.post_message(self.Selected(self.file.key))
289
+ else:
290
+ self.post_message(self.Unselect(self.file.key))
291
+ event.prevent_default()
292
+ event.stop()
293
+
294
+ def uncheck(self):
295
+ """Uncheck the file's checkbox."""
296
+ checkbox = self.query_one(Checkbox)
297
+ checkbox.value = False
298
+
299
+ def check(self):
300
+ """Check the file's checkbox."""
301
+ checkbox = self.query_one(Checkbox)
302
+ checkbox.value = True
303
+
304
+
305
+ class StorageContentContainer(Vertical):
306
+ """Main widget for displaying storage content.
307
+
308
+ This widget manages the display of storage content including files and folders,
309
+ handles file selection, search functionality, and bulk operations.
310
+
311
+ Attributes:
312
+ storage: The name of the current storage provider
313
+ path: The current path in the storage
314
+ search_prefix: The current search filter
315
+ access_credentials_uuid: UUID of the access credentials being used
316
+ storage_content: The current storage content to display
317
+ selected_files: Set of selected file names
318
+ selected_files_n: Number of selected files
319
+ """
320
+
321
+ storage: reactive[str | None] = reactive(None, recompose=True)
322
+ path: reactive[str | None] = reactive(None, recompose=False)
323
+ search_prefix: reactive[str | None] = reactive(None, recompose=False)
324
+ access_credentials_uuid: reactive[str | None] = reactive("", recompose=False)
325
+ storage_content: reactive[StorageContent | None] = reactive(None, recompose=True)
326
+ selected_files: reactive[set] = reactive(set(), recompose=False)
327
+ selected_files_n: reactive[int] = reactive(0, recompose=False)
328
+
329
+ DEFAULT_CSS = """
330
+
331
+ StorageContent {
332
+ padding: 1 2 1 1;
333
+ height: 100%
334
+ }
335
+
336
+ Horizontal {
337
+ height:auto
338
+ }
339
+ VerticalScroll {
340
+ height: 100%
341
+ }
342
+
343
+ .file_name {
344
+ width: 55%;
345
+ }
346
+
347
+ .file_size {
348
+ width: 10;
349
+ }
350
+
351
+ .file_date {
352
+ width: 25%;
353
+ }
354
+
355
+ .preview {
356
+ width: 5%;
357
+ }
358
+
359
+ #storage_path {
360
+ width: 100%;
361
+ height: auto;
362
+ border-bottom: solid $secondary;
363
+ margin: 1 0;
364
+ }
365
+ .storage_path_item {
366
+ padding: 0 0;
367
+ }
368
+
369
+ #search_input {
370
+ height: 1;
371
+ border: none;
372
+ background: transparent
373
+ }
374
+
375
+ Center {
376
+
377
+ & > Static {
378
+ width: auto;
379
+ }
380
+ }
381
+
382
+ .file_list_header {
383
+ border-bottom: solid $background-lighten-3;
384
+ }
385
+
386
+ #content {
387
+ height: 80%;
388
+ & > FileItem {
389
+ & > Checkbox {
390
+ display: none;
391
+ }
392
+ }
393
+
394
+ &.-visible {
395
+ & > FileItem {
396
+ & > Checkbox {
397
+ display: block;
398
+ }
399
+ }
400
+
401
+ }
402
+ }
403
+ #totals_section {
404
+ height:1;
405
+ padding-right: 1;
406
+ }
407
+
408
+ #default_actions {
409
+ width: 100%;
410
+ display: none;
411
+ align-horizontal: right;
412
+ height: auto;
413
+ padding-right: 2;
414
+ margin: 0 0;
415
+
416
+ &.-visible {
417
+ display: block;
418
+ }
419
+ Static {
420
+ width: auto;
421
+ height: auto;
422
+ }
423
+ }
424
+
425
+ #selected_actions {
426
+ width: 100%;
427
+ display: none;
428
+ layout: grid;
429
+ grid-size: 2 1;
430
+ height: auto;
431
+
432
+ &.-visible {
433
+ display: block;
434
+ }
435
+ Static {
436
+ width: auto;
437
+ height: auto;
438
+ }
439
+
440
+ #action_buttons {
441
+
442
+ align-horizontal: right;
443
+ height: auto;
444
+
445
+ & > Label {
446
+ width: auto;
447
+ padding-right: 2;
448
+
449
+
450
+ }
451
+ }
452
+ }
453
+ """
454
+
455
+ def compose(self) -> ComposeResult:
456
+ if not self.storage:
457
+ return
458
+ breadcrumbs = self.path.split("/") if self.path else []
459
+ breadcrumbs.insert(0, self.storage)
460
+
461
+ with Container(id="storage_path"):
462
+ with Horizontal():
463
+ yield Label("Current Path: ", classes="storage_path_item")
464
+ for index, breadcrumb in enumerate(breadcrumbs):
465
+ color = "$primary" if index == 0 else "$secondary"
466
+ yield PathSelector(
467
+ renderable=f"[{color}]{breadcrumb}[/]",
468
+ storage=self.storage,
469
+ path="/".join(breadcrumbs[1 : index + 1]),
470
+ access_credentials_uuid=self.access_credentials_uuid,
471
+ classes="storage_path_item",
472
+ )
473
+ yield Label("/", classes="storage_path_item")
474
+ with Horizontal():
475
+ yield Label("Search:")
476
+ yield Input(
477
+ id="search_input",
478
+ placeholder="input path prefix here...",
479
+ value=self.search_prefix,
480
+ )
481
+ if not self.storage_content:
482
+ return
483
+ with Horizontal(id="totals_section"):
484
+ with Horizontal(id="selected_actions"):
485
+ yield Button("❌Selected: ", id="selected_n", name="uncheck_all")
486
+ with Horizontal(id="action_buttons"):
487
+ yield Button(f"{DOWNLOAD_ICON} Download", name="download")
488
+ yield Button("🗑️ Delete", name="delete")
489
+ with Horizontal(id="default_actions", classes="-visible"):
490
+ yield Button(f"{UPLOAD_ICON} Upload", name="upload")
491
+ if not self.storage_content or (
492
+ not self.storage_content.files and not self.storage_content.folders
493
+ ):
494
+ with Middle():
495
+ with Center():
496
+ yield Static(NO_DATA_LOGO)
497
+ return
498
+ with Horizontal(classes="file_list_header"):
499
+ yield FileMetaLabel("Name", classes="file_name")
500
+ yield FileMetaLabel("Size", classes="file_size")
501
+ yield FileMetaLabel("Date modified", classes="file_date")
502
+ yield FileMetaLabel("Preview", classes="preview")
503
+ with VerticalScroll(id="content"):
504
+ for folder in self.storage_content.folders:
505
+ yield FolderItem(
506
+ self.storage, self.access_credentials_uuid, self.path, folder
507
+ )
508
+ for file in self.storage_content.files:
509
+ yield FileItem(self.storage, self.path, file, id=file.uuid)
510
+
511
+ @on(Input.Submitted)
512
+ def on_input_submitted(self, event: Input.Submitted):
513
+ """
514
+ Handle input submission events to apply the search prefix.
515
+
516
+ This method is triggered when the user presses Enter in the input field
517
+ and applies the search prefix to the current storage content.
518
+
519
+ Args:
520
+ event (Input.Submitted): The submit event containing the input value
521
+ """
522
+ self.apply_search_prefix(event.value)
523
+
524
+ @on(Input.Blurred)
525
+ def on_input_blurred(self, event: Input.Blurred):
526
+ """
527
+ Handle input blur events to apply the search prefix.
528
+
529
+ This method is triggered when the input field loses focus and applies
530
+ the search prefix to the current storage content.
531
+
532
+ Args:
533
+ event (Input.Blurred): The blur event containing the input value
534
+ """
535
+ self.apply_search_prefix(event.value)
536
+
537
+ @on(FileItem.Preview)
538
+ def on_file_item_preview(self, event: FileItem.Preview):
539
+ """
540
+ Handle file preview events to request a preview of the selected file.
541
+ This method sends a PreviewRequest message with the storage name,
542
+ access credentials UUID, and file path to the backend for processing.
543
+
544
+ Args:
545
+ event (FileItem.Preview): The preview event containing the file name
546
+
547
+ """
548
+ if not self.storage or not self.access_credentials_uuid:
549
+ return
550
+ self.post_message(
551
+ PreviewRequest(
552
+ self.storage,
553
+ self.access_credentials_uuid,
554
+ os.path.join(self.path, event.name) if self.path else event.name,
555
+ )
556
+ )
557
+
558
+ @on(FileItem.Selected)
559
+ def on_file_item_select(self, event: FileItem.Selected):
560
+ """
561
+ Handle file selection events to update the selected files list.
562
+
563
+ This method adds the selected file to the selected_files set and updates
564
+ the selected_files_n attribute accordingly. It also manages the visibility of
565
+ the selected actions and default actions sections based on the number of selected files.
566
+
567
+ Args:
568
+ event (FileItem.Selected): The select event containing the file name
569
+ """
570
+ self.selected_files.add(event.name)
571
+ self.selected_files_n = len(self.selected_files)
572
+
573
+ selected_actions = self.query_one("#selected_actions")
574
+ content = self.query_one("#content")
575
+
576
+ if not content.has_class("-visible"):
577
+ content.add_class("-visible")
578
+ if not selected_actions.has_class("-visible"):
579
+ selected_actions.add_class("-visible")
580
+
581
+ self.query_one("#default_actions").remove_class("-visible")
582
+
583
+ @on(FileItem.Unselect)
584
+ def on_file_item_unselect(self, event: FileItem.Unselect):
585
+ """
586
+ Handle file unselection events to update the selected files list.
587
+
588
+ This method removes the unselected file from the selected_files set and updates
589
+ the selected_files_n attribute accordingly. It also manages the visibility of
590
+ the selected actions and default actions sections based on the number of selected files.
591
+ Args:
592
+ event (FileItem.Unselect): The unselect event containing the file name
593
+ """
594
+ if event.name not in self.selected_files:
595
+ return
596
+ self.selected_files.remove(event.name)
597
+ self.selected_files_n = len(self.selected_files)
598
+ if self.selected_files_n == 0:
599
+ self.query_one("#content").remove_class("-visible")
600
+ self.query_one("#selected_actions").remove_class("-visible")
601
+ self.query_one("#default_actions").add_class("-visible")
602
+
603
+ @on(Button.Click)
604
+ def on_button_click(self, event: Button.Click):
605
+ """
606
+ Handle button click events to perform actions on selected files.
607
+ This method processes the click events for buttons in the storage content
608
+ section, such as upload, delete, download, and uncheck all actions.
609
+
610
+ Args:
611
+ event (Button.Click): The button click event
612
+ """
613
+ if not self.storage or not self.access_credentials_uuid:
614
+ return
615
+ params = {
616
+ "storage_name": self.storage,
617
+ "path": self.path,
618
+ "access_credentials_uuid": self.access_credentials_uuid,
619
+ "keys": self.selected_files,
620
+ }
621
+
622
+ # Convert string action to enum
623
+ action_type = ActionType.from_string(event.action)
624
+
625
+ if action_type == ActionType.UPLOAD:
626
+ self.post_message(
627
+ UploadRequest(
628
+ access_credentials_uuid=self.access_credentials_uuid,
629
+ storage=self.storage,
630
+ path=self.path,
631
+ )
632
+ )
633
+ elif action_type == ActionType.DELETE:
634
+ self.post_message(DeleteRequest(**params))
635
+ elif action_type == ActionType.DOWNLOAD:
636
+ self.post_message(DownloadRequest(**params))
637
+ elif action_type == ActionType.UNCHECK_ALL:
638
+ self.post_message(
639
+ UncheckFilesRequest(
640
+ keys=[
641
+ item.uuid
642
+ for item in self.storage_content.files # type: ignore
643
+ if item.key in self.selected_files
644
+ ]
645
+ )
646
+ )
647
+ self.query_one("#default_actions").add_class("-visible")
648
+
649
+ def watch_selected_files_n(self):
650
+ """
651
+ Watch for changes in the number of selected files and update the UI accordingly.
652
+
653
+ This method updates the visibility and content of the selected actions section
654
+ based on the number of selected files.
655
+ """
656
+ try:
657
+ selected_actions = self.query_one("#selected_actions")
658
+ counter = self.query_one("#selected_n")
659
+ except NoMatches:
660
+ return
661
+
662
+ if self.selected_files_n > 0:
663
+ if not selected_actions.has_class("-visible"):
664
+ selected_actions.add_class("-visible")
665
+ counter.update(f"❌Selected: {self.selected_files_n}") # type: ignore
666
+ else:
667
+ selected_actions.remove_class("-visible")
668
+ self.query_one("#content").remove_class("-visible")
669
+ self.query_one("#selected_actions").remove_class("-visible")
670
+
671
+ def apply_search_prefix(self, value):
672
+ """
673
+ Apply a search prefix filter to the current storage content.
674
+
675
+ This method updates the search prefix and triggers a SelectStorageItem
676
+ message to refresh the storage content with the new filter.
677
+
678
+ Args:
679
+ value (str): The search prefix to apply
680
+ """
681
+ if not self.storage:
682
+ return
683
+ self.search_prefix = value
684
+ self.post_message(
685
+ SelectStorageItem(
686
+ self.storage, # type: ignore
687
+ self.path,
688
+ self.access_credentials_uuid,
689
+ value,
690
+ )
691
+ )