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.
- data_sourcerer-0.1.0.dist-info/METADATA +52 -0
- data_sourcerer-0.1.0.dist-info/RECORD +95 -0
- data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
- data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
- data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
- data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
- sourcerer/__init__.py +15 -0
- sourcerer/domain/__init__.py +13 -0
- sourcerer/domain/access_credentials/__init__.py +7 -0
- sourcerer/domain/access_credentials/entities.py +53 -0
- sourcerer/domain/access_credentials/exceptions.py +17 -0
- sourcerer/domain/access_credentials/repositories.py +84 -0
- sourcerer/domain/access_credentials/services.py +91 -0
- sourcerer/domain/file_system/__init__.py +7 -0
- sourcerer/domain/file_system/entities.py +70 -0
- sourcerer/domain/file_system/exceptions.py +17 -0
- sourcerer/domain/file_system/services.py +64 -0
- sourcerer/domain/shared/__init__.py +6 -0
- sourcerer/domain/shared/entities.py +18 -0
- sourcerer/domain/storage_provider/__init__.py +7 -0
- sourcerer/domain/storage_provider/entities.py +86 -0
- sourcerer/domain/storage_provider/exceptions.py +17 -0
- sourcerer/domain/storage_provider/services.py +130 -0
- sourcerer/infrastructure/__init__.py +13 -0
- sourcerer/infrastructure/access_credentials/__init__.py +7 -0
- sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
- sourcerer/infrastructure/access_credentials/registry.py +120 -0
- sourcerer/infrastructure/access_credentials/repositories.py +119 -0
- sourcerer/infrastructure/access_credentials/services.py +396 -0
- sourcerer/infrastructure/db/__init__.py +6 -0
- sourcerer/infrastructure/db/config.py +73 -0
- sourcerer/infrastructure/db/models.py +47 -0
- sourcerer/infrastructure/file_system/__init__.py +7 -0
- sourcerer/infrastructure/file_system/exceptions.py +89 -0
- sourcerer/infrastructure/file_system/services.py +147 -0
- sourcerer/infrastructure/storage_provider/__init__.py +7 -0
- sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
- sourcerer/infrastructure/storage_provider/registry.py +84 -0
- sourcerer/infrastructure/storage_provider/services.py +509 -0
- sourcerer/infrastructure/utils.py +106 -0
- sourcerer/presentation/__init__.py +12 -0
- sourcerer/presentation/app.py +36 -0
- sourcerer/presentation/di_container.py +46 -0
- sourcerer/presentation/screens/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/main.py +78 -0
- sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
- sourcerer/presentation/screens/file_system_finder/main.py +248 -0
- sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
- sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
- sourcerer/presentation/screens/main/__init__.py +0 -0
- sourcerer/presentation/screens/main/main.py +469 -0
- sourcerer/presentation/screens/main/messages/__init__.py +0 -0
- sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
- sourcerer/presentation/screens/main/messages/download_request.py +12 -0
- sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
- sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
- sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
- sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
- sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
- sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
- sourcerer/presentation/screens/main/styles.tcss +32 -0
- sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
- sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
- sourcerer/presentation/screens/preview_content/__init__.py +0 -0
- sourcerer/presentation/screens/preview_content/main.py +59 -0
- sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
- sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
- sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
- sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
- sourcerer/presentation/screens/question/__init__.py +0 -0
- sourcerer/presentation/screens/question/main.py +31 -0
- sourcerer/presentation/screens/question/styles.tcss +33 -0
- sourcerer/presentation/screens/shared/__init__.py +0 -0
- sourcerer/presentation/screens/shared/containers.py +13 -0
- sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/shared/widgets/button.py +54 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
- sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
- sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
- sourcerer/presentation/settings.py +31 -0
- sourcerer/presentation/themes/__init__.py +0 -0
- sourcerer/presentation/themes/github_dark.py +21 -0
- sourcerer/presentation/utils.py +69 -0
- sourcerer/settings.py +72 -0
- 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
|
+
)
|