data-sourcerer 0.2.2__py3-none-any.whl → 0.3.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.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/METADATA +1 -1
- {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/RECORD +43 -29
- sourcerer/__init__.py +1 -1
- sourcerer/domain/access_credentials/entities.py +3 -1
- sourcerer/domain/access_credentials/repositories.py +1 -1
- sourcerer/domain/storage/__init__.py +0 -0
- sourcerer/domain/storage/entities.py +27 -0
- sourcerer/domain/storage/repositories.py +31 -0
- sourcerer/infrastructure/access_credentials/repositories.py +3 -2
- sourcerer/infrastructure/access_credentials/services.py +9 -25
- sourcerer/infrastructure/db/models.py +33 -2
- sourcerer/infrastructure/storage/__init__.py +0 -0
- sourcerer/infrastructure/storage/repositories.py +72 -0
- sourcerer/infrastructure/storage/services.py +37 -0
- sourcerer/infrastructure/storage_provider/services/gcp.py +1 -1
- sourcerer/infrastructure/utils.py +2 -1
- sourcerer/presentation/di_container.py +15 -0
- sourcerer/presentation/screens/file_system_finder/main.py +7 -6
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
- sourcerer/presentation/screens/main/main.py +63 -8
- sourcerer/presentation/screens/main/messages/select_storage_item.py +1 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +2 -1
- sourcerer/presentation/screens/main/widgets/storage_content.py +191 -85
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
- sourcerer/presentation/screens/preview_content/main.py +15 -3
- sourcerer/presentation/screens/provider_creds_list/main.py +29 -8
- sourcerer/presentation/screens/provider_creds_registration/main.py +7 -4
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +2 -2
- sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +7 -11
- sourcerer/presentation/screens/storages_list/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/main.py +180 -0
- sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +8 -0
- sourcerer/presentation/screens/storages_list/styles.tcss +55 -0
- sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
- sourcerer/presentation/screens/storages_registration/main.py +100 -0
- sourcerer/presentation/screens/storages_registration/styles.tcss +41 -0
- sourcerer/presentation/settings.py +29 -16
- sourcerer/presentation/utils.py +9 -1
- sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
- {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.2.2.dist-info → data_sourcerer-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,18 +7,20 @@ display with search functionality.
|
|
7
7
|
|
8
8
|
import contextlib
|
9
9
|
import os.path
|
10
|
+
from abc import abstractmethod
|
10
11
|
from dataclasses import dataclass
|
11
12
|
from enum import Enum, auto
|
13
|
+
from typing import ClassVar, Self
|
12
14
|
|
13
15
|
from textual import events, on
|
14
16
|
from textual.app import ComposeResult
|
17
|
+
from textual.binding import Binding, BindingType
|
15
18
|
from textual.containers import (
|
16
19
|
Center,
|
17
20
|
Container,
|
18
21
|
Horizontal,
|
19
22
|
Middle,
|
20
23
|
Vertical,
|
21
|
-
VerticalScroll,
|
22
24
|
)
|
23
25
|
from textual.css.query import NoMatches
|
24
26
|
from textual.message import Message
|
@@ -38,8 +40,11 @@ from sourcerer.presentation.screens.main.messages.uncheck_files_request import (
|
|
38
40
|
UncheckFilesRequest,
|
39
41
|
)
|
40
42
|
from sourcerer.presentation.screens.main.messages.upload_request import UploadRequest
|
43
|
+
from sourcerer.presentation.screens.shared.containers import (
|
44
|
+
ScrollVerticalContainerWithNoBindings,
|
45
|
+
)
|
41
46
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
42
|
-
from sourcerer.presentation.settings import NO_DATA_LOGO
|
47
|
+
from sourcerer.presentation.settings import NO_DATA_LOGO, KeyBindings
|
43
48
|
from sourcerer.settings import (
|
44
49
|
DIRECTORY_ICON,
|
45
50
|
DOWNLOAD_ICON,
|
@@ -91,6 +96,10 @@ class ActionType(Enum):
|
|
91
96
|
return action_map[action_str]
|
92
97
|
|
93
98
|
|
99
|
+
class UnfocusableCheckbox(Checkbox):
|
100
|
+
can_focus = False
|
101
|
+
|
102
|
+
|
94
103
|
class FileMetaLabel(Static):
|
95
104
|
"""Widget for displaying file metadata information.
|
96
105
|
|
@@ -120,6 +129,16 @@ class PathSelector(Label):
|
|
120
129
|
access_credentials_uuid: UUID of the access credentials being used
|
121
130
|
"""
|
122
131
|
|
132
|
+
can_focus = True
|
133
|
+
|
134
|
+
DEFAULT_CSS = """
|
135
|
+
PathSelector {
|
136
|
+
&:focus {
|
137
|
+
background: $secondary-lighten-2;
|
138
|
+
}
|
139
|
+
}
|
140
|
+
"""
|
141
|
+
|
123
142
|
def __init__(self, storage, path, access_credentials_uuid, *args, **kwargs):
|
124
143
|
super().__init__(*args, **kwargs)
|
125
144
|
self.storage = storage
|
@@ -128,31 +147,93 @@ class PathSelector(Label):
|
|
128
147
|
|
129
148
|
def on_click(self, _: events.Click) -> None:
|
130
149
|
"""Handle click events to navigate to the selected path."""
|
150
|
+
self._select()
|
151
|
+
|
152
|
+
def on_key(self, event: events.Key) -> None:
|
153
|
+
"""Handle key events to navigate to the selected path."""
|
154
|
+
if event.key == KeyBindings.ENTER.value:
|
155
|
+
self._select()
|
156
|
+
|
157
|
+
def _select(self):
|
158
|
+
"""Select the current path."""
|
131
159
|
self.post_message(
|
132
|
-
SelectStorageItem(
|
160
|
+
SelectStorageItem(
|
161
|
+
self.storage,
|
162
|
+
self.path,
|
163
|
+
self.access_credentials_uuid,
|
164
|
+
focus_content=True,
|
165
|
+
)
|
133
166
|
)
|
134
167
|
|
135
168
|
|
136
|
-
class
|
169
|
+
class StorageContentItem(Horizontal):
|
170
|
+
DEFAULT_CSS = """
|
171
|
+
StorageContentItem.active {
|
172
|
+
background: $secondary;
|
173
|
+
color: $panel;
|
174
|
+
}
|
175
|
+
StorageContentItem:focus {
|
176
|
+
background: $secondary-lighten-2;
|
177
|
+
color: $panel;
|
178
|
+
}
|
179
|
+
"""
|
180
|
+
|
181
|
+
can_focus = True
|
182
|
+
|
183
|
+
def __init__(self, focus_first: bool, *args, **kwargs):
|
184
|
+
"""Initialize the storage content widget."""
|
185
|
+
super().__init__(*args, **kwargs)
|
186
|
+
self.focus_first = focus_first
|
187
|
+
|
188
|
+
def on_mount(self) -> None:
|
189
|
+
"""Handle the mounting of the widget."""
|
190
|
+
if self.focus_first and self.first_child:
|
191
|
+
self.focus()
|
192
|
+
|
193
|
+
@abstractmethod
|
194
|
+
def _select(self, widget=None):
|
195
|
+
raise NotImplementedError
|
196
|
+
|
197
|
+
def on_click(self, event: events.Click) -> None:
|
198
|
+
"""Handle click events to navigate into the folder."""
|
199
|
+
self._select(event.widget)
|
200
|
+
|
201
|
+
def on_key(self, event: events.Key) -> None:
|
202
|
+
"""Handle key events to navigate into the folder."""
|
203
|
+
if event.key == KeyBindings.ARROW_UP.value:
|
204
|
+
if self.first_child:
|
205
|
+
self.parent.children[-1].focus() # type: ignore
|
206
|
+
return
|
207
|
+
self.screen.focus_previous()
|
208
|
+
if event.key == KeyBindings.ARROW_DOWN.value:
|
209
|
+
if self.last_child:
|
210
|
+
self.parent.children[0].focus() # type: ignore
|
211
|
+
return
|
212
|
+
self.screen.focus_next()
|
213
|
+
|
214
|
+
@on(events.Enter)
|
215
|
+
@on(events.Leave)
|
216
|
+
def on_enter(self, _: events.Enter):
|
217
|
+
with contextlib.suppress(Exception):
|
218
|
+
self.set_class(self.is_mouse_over, "active")
|
219
|
+
|
220
|
+
|
221
|
+
class FolderItem(StorageContentItem):
|
137
222
|
"""Widget for displaying and interacting with folder items.
|
138
223
|
|
139
224
|
This widget represents a folder in the storage content view, allowing
|
140
225
|
navigation into the folder and visual feedback on hover/selection.
|
141
226
|
"""
|
142
227
|
|
143
|
-
DEFAULT_CSS = """
|
144
|
-
FolderItem {
|
145
|
-
margin-bottom: 1;
|
146
|
-
}
|
147
|
-
FolderItem.active {
|
148
|
-
background: $secondary;
|
149
|
-
color: $panel;
|
150
|
-
}
|
151
|
-
"""
|
152
|
-
can_focus = False
|
153
|
-
|
154
228
|
def __init__(
|
155
|
-
self,
|
229
|
+
self,
|
230
|
+
storage,
|
231
|
+
access_credentials_uuid,
|
232
|
+
parent_path,
|
233
|
+
folder,
|
234
|
+
focus_first,
|
235
|
+
*args,
|
236
|
+
**kwargs,
|
156
237
|
):
|
157
238
|
"""Initialize a folder item widget.
|
158
239
|
|
@@ -162,7 +243,7 @@ class FolderItem(Horizontal):
|
|
162
243
|
parent_path: The parent path of the folder
|
163
244
|
folder: The folder name
|
164
245
|
"""
|
165
|
-
super().__init__(*args, **kwargs)
|
246
|
+
super().__init__(focus_first, *args, **kwargs)
|
166
247
|
self.storage = storage
|
167
248
|
self.access_credentials_uuid = access_credentials_uuid
|
168
249
|
self.parent_path = parent_path
|
@@ -172,26 +253,29 @@ class FolderItem(Horizontal):
|
|
172
253
|
"""Compose the folder item layout with folder name and icon."""
|
173
254
|
yield Label(f"{DIRECTORY_ICON}{self.folder.key}", markup=False)
|
174
255
|
|
175
|
-
def
|
176
|
-
"""
|
256
|
+
def _select(self, widget=None):
|
257
|
+
"""Select the folder."""
|
177
258
|
path = self.folder.key
|
178
259
|
if self.parent_path:
|
179
260
|
path = self.parent_path.strip("/") + "/" + path
|
180
261
|
|
181
262
|
self.post_message(
|
182
|
-
SelectStorageItem(
|
263
|
+
SelectStorageItem(
|
264
|
+
self.storage, path, self.access_credentials_uuid, focus_content=True
|
265
|
+
)
|
183
266
|
)
|
184
267
|
|
185
|
-
def
|
186
|
-
"""Handle
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
268
|
+
def on_key(self, event: events.Key) -> None:
|
269
|
+
"""Handle key events to navigate into the folder."""
|
270
|
+
if event.key in (KeyBindings.ARROW_UP.value, KeyBindings.ARROW_DOWN.value):
|
271
|
+
event.prevent_default()
|
272
|
+
if event.key == KeyBindings.ENTER.value:
|
273
|
+
self._select()
|
274
|
+
return
|
275
|
+
super().on_key(event)
|
192
276
|
|
193
277
|
|
194
|
-
class FileItem(
|
278
|
+
class FileItem(StorageContentItem):
|
195
279
|
"""Widget for displaying and interacting with file items.
|
196
280
|
|
197
281
|
This widget represents a file in the storage content view, allowing
|
@@ -199,17 +283,10 @@ class FileItem(Horizontal):
|
|
199
283
|
"""
|
200
284
|
|
201
285
|
DEFAULT_CSS = """
|
202
|
-
FileItem {
|
203
|
-
margin-bottom: 1;
|
204
|
-
}
|
205
|
-
FileItem.active {
|
206
|
-
background: $secondary;
|
207
|
-
color: $panel;
|
208
|
-
}
|
209
286
|
.file_size {
|
210
287
|
color: $primary
|
211
288
|
}
|
212
|
-
|
289
|
+
UnfocusableCheckbox {
|
213
290
|
border: none;
|
214
291
|
padding: 0 0;
|
215
292
|
display: none;
|
@@ -219,7 +296,6 @@ class FileItem(Horizontal):
|
|
219
296
|
}
|
220
297
|
}
|
221
298
|
"""
|
222
|
-
can_focus = False
|
223
299
|
|
224
300
|
@dataclass
|
225
301
|
class Selected(Message):
|
@@ -239,11 +315,7 @@ class FileItem(Horizontal):
|
|
239
315
|
|
240
316
|
name: str
|
241
317
|
|
242
|
-
def
|
243
|
-
"""Initialize the file item on mount."""
|
244
|
-
self.add_class("file-item")
|
245
|
-
|
246
|
-
def __init__(self, storage, parent_path, file, *args, **kwargs):
|
318
|
+
def __init__(self, storage, parent_path, file, focus_first, *args, **kwargs):
|
247
319
|
"""Initialize a file item widget.
|
248
320
|
|
249
321
|
Args:
|
@@ -251,13 +323,13 @@ class FileItem(Horizontal):
|
|
251
323
|
parent_path: The parent path of the file
|
252
324
|
file: The file name
|
253
325
|
"""
|
254
|
-
super().__init__(*args, **kwargs)
|
326
|
+
super().__init__(focus_first, *args, **kwargs)
|
255
327
|
self.storage = storage
|
256
328
|
self.parent_path = parent_path
|
257
329
|
self.file = file
|
258
330
|
|
259
331
|
def compose(self):
|
260
|
-
yield
|
332
|
+
yield UnfocusableCheckbox()
|
261
333
|
yield FileMetaLabel(
|
262
334
|
f"{FILE_ICON} {self.file.key}", classes="file_name", markup=False
|
263
335
|
)
|
@@ -268,42 +340,45 @@ class FileItem(Horizontal):
|
|
268
340
|
if self.file.is_text:
|
269
341
|
yield Button(f"{PREVIEW_ICON}", name="preview", classes="download")
|
270
342
|
|
271
|
-
def
|
272
|
-
"""Handle
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
343
|
+
def on_key(self, event: events.Key) -> None:
|
344
|
+
"""Handle key events to toggle file selection."""
|
345
|
+
if event.key in (KeyBindings.ARROW_UP.value, KeyBindings.ARROW_DOWN.value):
|
346
|
+
event.prevent_default()
|
347
|
+
if event.key == KeyBindings.ENTER.value:
|
348
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
349
|
+
checkbox.value = not checkbox.value
|
350
|
+
if checkbox.value:
|
351
|
+
self.post_message(self.Selected(self.file.key))
|
352
|
+
else:
|
353
|
+
self.post_message(self.Unselect(self.file.key))
|
354
|
+
return
|
355
|
+
super().on_key(event)
|
278
356
|
|
279
|
-
def
|
280
|
-
"""Handle click events to toggle file selection."""
|
357
|
+
def _select(self, widget=None):
|
281
358
|
preview_button = None
|
282
359
|
with contextlib.suppress(NoMatches):
|
283
360
|
preview_button = self.query_one(Button)
|
284
361
|
|
285
|
-
if
|
362
|
+
if widget is preview_button:
|
286
363
|
self.post_message(self.Preview(self.file.key))
|
287
364
|
return
|
288
365
|
|
289
|
-
checkbox = self.query_one(
|
290
|
-
if
|
366
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
367
|
+
if widget is not checkbox:
|
291
368
|
checkbox.value = not checkbox.value
|
292
369
|
if checkbox.value:
|
293
370
|
self.post_message(self.Selected(self.file.key))
|
294
371
|
else:
|
295
372
|
self.post_message(self.Unselect(self.file.key))
|
296
|
-
event.prevent_default()
|
297
|
-
event.stop()
|
298
373
|
|
299
374
|
def uncheck(self):
|
300
375
|
"""Uncheck the file's checkbox."""
|
301
|
-
checkbox = self.query_one(
|
376
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
302
377
|
checkbox.value = False
|
303
378
|
|
304
379
|
def check(self):
|
305
380
|
"""Check the file's checkbox."""
|
306
|
-
checkbox = self.query_one(
|
381
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
307
382
|
checkbox.value = True
|
308
383
|
|
309
384
|
|
@@ -332,18 +407,24 @@ class StorageContentContainer(Vertical):
|
|
332
407
|
search_prefix: reactive[str | None] = reactive( # ty: ignore[invalid-assignment]
|
333
408
|
None, recompose=False
|
334
409
|
)
|
335
|
-
access_credentials_uuid: reactive[
|
410
|
+
access_credentials_uuid: reactive[ # ty: ignore[invalid-assignment]
|
336
411
|
str | None
|
337
|
-
] = reactive(
|
338
|
-
|
339
|
-
)
|
340
|
-
storage_content: reactive[
|
412
|
+
] = reactive("", recompose=False)
|
413
|
+
storage_content: reactive[ # ty: ignore[invalid-assignment]
|
341
414
|
StorageContent | None
|
342
|
-
] = reactive(
|
343
|
-
None, recompose=True
|
344
|
-
)
|
415
|
+
] = reactive(None, recompose=True)
|
345
416
|
selected_files: reactive[set] = reactive(set(), recompose=False)
|
346
417
|
selected_files_n: reactive[int] = reactive(0, recompose=False)
|
418
|
+
focus_content: reactive[bool] = reactive(False, recompose=False)
|
419
|
+
|
420
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
421
|
+
Binding(
|
422
|
+
f"{KeyBindings.CTRL.value}+{KeyBindings.BACKSPACE.value}",
|
423
|
+
"back_to_prev_path",
|
424
|
+
"Navigate back to the previous path",
|
425
|
+
show=True,
|
426
|
+
),
|
427
|
+
]
|
347
428
|
|
348
429
|
DEFAULT_CSS = """
|
349
430
|
|
@@ -379,7 +460,7 @@ class StorageContentContainer(Vertical):
|
|
379
460
|
width: 100%;
|
380
461
|
height: auto;
|
381
462
|
border-bottom: solid $secondary;
|
382
|
-
margin: 1 0;
|
463
|
+
margin: 1 0 0 0;
|
383
464
|
|
384
465
|
PathSelector {
|
385
466
|
&.primary_color {
|
@@ -528,13 +609,28 @@ class StorageContentContainer(Vertical):
|
|
528
609
|
yield FileMetaLabel("Size", classes="file_size")
|
529
610
|
yield FileMetaLabel("Date modified", classes="file_date")
|
530
611
|
yield FileMetaLabel("Preview", classes="preview")
|
531
|
-
with
|
612
|
+
with ScrollVerticalContainerWithNoBindings(id="content", can_focus=False):
|
532
613
|
for folder in self.storage_content.folders:
|
533
614
|
yield FolderItem(
|
534
|
-
self.storage,
|
615
|
+
self.storage,
|
616
|
+
self.access_credentials_uuid,
|
617
|
+
self.path,
|
618
|
+
folder,
|
619
|
+
self.focus_content,
|
535
620
|
)
|
536
621
|
for file in self.storage_content.files:
|
537
|
-
yield FileItem(
|
622
|
+
yield FileItem(
|
623
|
+
self.storage, self.path, file, self.focus_content, id=file.uuid
|
624
|
+
)
|
625
|
+
|
626
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
627
|
+
try:
|
628
|
+
content = self.query_one(ScrollVerticalContainerWithNoBindings)
|
629
|
+
except NoMatches:
|
630
|
+
return self
|
631
|
+
if len(content.children) > 0:
|
632
|
+
content.children[0].focus()
|
633
|
+
return self
|
538
634
|
|
539
635
|
@on(Input.Submitted)
|
540
636
|
def on_input_submitted(self, event: Input.Submitted):
|
@@ -549,19 +645,6 @@ class StorageContentContainer(Vertical):
|
|
549
645
|
"""
|
550
646
|
self.apply_search_prefix(event.value)
|
551
647
|
|
552
|
-
@on(Input.Blurred)
|
553
|
-
def on_input_blurred(self, event: Input.Blurred):
|
554
|
-
"""
|
555
|
-
Handle input blur events to apply the search prefix.
|
556
|
-
|
557
|
-
This method is triggered when the input field loses focus and applies
|
558
|
-
the search prefix to the current storage content.
|
559
|
-
|
560
|
-
Args:
|
561
|
-
event (Input.Blurred): The blur event containing the input value
|
562
|
-
"""
|
563
|
-
self.apply_search_prefix(event.value)
|
564
|
-
|
565
648
|
@on(FileItem.Preview)
|
566
649
|
def on_file_item_preview(self, event: FileItem.Preview):
|
567
650
|
"""
|
@@ -715,5 +798,28 @@ class StorageContentContainer(Vertical):
|
|
715
798
|
self.path,
|
716
799
|
self.access_credentials_uuid,
|
717
800
|
value,
|
801
|
+
focus_content=True,
|
802
|
+
)
|
803
|
+
)
|
804
|
+
|
805
|
+
def action_back_to_prev_path(self):
|
806
|
+
"""
|
807
|
+
Navigate back to the previous path in the storage content.
|
808
|
+
|
809
|
+
This method updates the path to the parent directory and triggers a
|
810
|
+
SelectStorageItem message to refresh the storage content with the new path.
|
811
|
+
"""
|
812
|
+
if not self.storage:
|
813
|
+
return
|
814
|
+
if not self.path:
|
815
|
+
return
|
816
|
+
path_parents = [i for i in self.path.split("/")[:-1] if i]
|
817
|
+
prev_path = "/".join(path_parents)
|
818
|
+
self.post_message(
|
819
|
+
SelectStorageItem(
|
820
|
+
self.storage,
|
821
|
+
prev_path,
|
822
|
+
self.access_credentials_uuid,
|
823
|
+
focus_content=True,
|
718
824
|
)
|
719
825
|
)
|
@@ -7,10 +7,12 @@ and selection of storage items.
|
|
7
7
|
|
8
8
|
from collections import namedtuple
|
9
9
|
from itertools import groupby
|
10
|
+
from typing import Self
|
10
11
|
|
11
12
|
from textual import events, on
|
12
13
|
from textual.app import ComposeResult
|
13
|
-
from textual.containers import
|
14
|
+
from textual.containers import Horizontal, Vertical
|
15
|
+
from textual.css.query import NoMatches
|
14
16
|
from textual.reactive import reactive
|
15
17
|
from textual.widgets import Label, Rule
|
16
18
|
|
@@ -23,8 +25,12 @@ from sourcerer.presentation.screens.main.messages.select_storage_item import (
|
|
23
25
|
SelectStorageItem,
|
24
26
|
)
|
25
27
|
from sourcerer.presentation.screens.main.widgets.gradient import GradientWidget
|
28
|
+
from sourcerer.presentation.screens.shared.containers import (
|
29
|
+
ScrollVerticalContainerWithNoBindings,
|
30
|
+
)
|
26
31
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
27
|
-
from sourcerer.presentation.screens.shared.widgets.
|
32
|
+
from sourcerer.presentation.screens.shared.widgets.spinner import Spinner
|
33
|
+
from sourcerer.presentation.settings import KeyBindings
|
28
34
|
|
29
35
|
STORAGE_ICONS = {
|
30
36
|
StorageProvider.S3: "🟠",
|
@@ -41,6 +47,9 @@ class StorageItem(Label):
|
|
41
47
|
selection and visual feedback on hover.
|
42
48
|
"""
|
43
49
|
|
50
|
+
can_focus = True
|
51
|
+
selected = reactive(False, recompose=True, toggle_class="selected")
|
52
|
+
|
44
53
|
DEFAULT_CSS = """
|
45
54
|
StorageItem {
|
46
55
|
width: 90%;
|
@@ -51,7 +60,17 @@ class StorageItem(Label):
|
|
51
60
|
text-wrap: nowrap;
|
52
61
|
|
53
62
|
& > :hover {
|
54
|
-
background: $
|
63
|
+
background: $primary-lighten-2;
|
64
|
+
color: $panel;
|
65
|
+
}
|
66
|
+
|
67
|
+
& > :focus {
|
68
|
+
background: $primary-lighten-2;
|
69
|
+
color: $panel;
|
70
|
+
}
|
71
|
+
|
72
|
+
&.selected {
|
73
|
+
background: $primary;
|
55
74
|
color: $panel;
|
56
75
|
}
|
57
76
|
}
|
@@ -71,6 +90,38 @@ class StorageItem(Label):
|
|
71
90
|
|
72
91
|
def on_click(self, _: events.Click) -> None:
|
73
92
|
"""Handle click events to select the storage item."""
|
93
|
+
self._select_storage()
|
94
|
+
|
95
|
+
def on_key(self, event: events.Key) -> None:
|
96
|
+
"""Handle key events to select the storage item."""
|
97
|
+
if event.key == KeyBindings.ENTER.value:
|
98
|
+
self._select_storage()
|
99
|
+
return
|
100
|
+
storages = [
|
101
|
+
component
|
102
|
+
for component in self.screen.focus_chain
|
103
|
+
if isinstance(component, StorageItem)
|
104
|
+
]
|
105
|
+
if not storages:
|
106
|
+
return
|
107
|
+
if event.key == KeyBindings.ARROW_DOWN.value:
|
108
|
+
if self.screen.focused == storages[-1]:
|
109
|
+
storages[0].focus()
|
110
|
+
return
|
111
|
+
self.screen.focus_next(StorageItem)
|
112
|
+
elif event.key == KeyBindings.ARROW_UP.value:
|
113
|
+
if self.screen.focused == storages[0]:
|
114
|
+
storages[-1].focus()
|
115
|
+
return
|
116
|
+
self.screen.focus_previous(StorageItem)
|
117
|
+
|
118
|
+
def _select_storage(self):
|
119
|
+
"""
|
120
|
+
Select the storage item and notify the application.
|
121
|
+
This method posts a message to select the storage item based on its
|
122
|
+
name and access credentials UUID.
|
123
|
+
|
124
|
+
"""
|
74
125
|
self.post_message(
|
75
126
|
SelectStorageItem(
|
76
127
|
self.storage_name, access_credentials_uuid=self.access_credentials_uuid
|
@@ -78,7 +129,7 @@ class StorageItem(Label):
|
|
78
129
|
)
|
79
130
|
|
80
131
|
|
81
|
-
class StorageListSidebar(
|
132
|
+
class StorageListSidebar(Vertical):
|
82
133
|
"""Sidebar widget for displaying the list of storage providers.
|
83
134
|
|
84
135
|
This widget manages the display of storage providers grouped by their type,
|
@@ -98,27 +149,29 @@ class StorageListSidebar(VerticalScroll):
|
|
98
149
|
StorageListSidebar {
|
99
150
|
padding-right: 0;
|
100
151
|
margin-right: 0;
|
101
|
-
|
152
|
+
height: 100%;
|
153
|
+
margin-bottom: 1;
|
154
|
+
#rule-left {
|
155
|
+
width: 1;
|
156
|
+
}
|
157
|
+
|
158
|
+
ScrollVerticalContainerWithNoBindings{
|
159
|
+
height: 95%;
|
160
|
+
}
|
161
|
+
|
162
|
+
Horizontal {
|
102
163
|
height: auto;
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
Horizontal {
|
109
|
-
height: auto;
|
110
|
-
}
|
111
|
-
Rule.-horizontal {
|
112
|
-
height: 1;
|
113
|
-
margin: 0 0;
|
114
|
-
|
115
|
-
}
|
116
|
-
.storage-letter {
|
117
|
-
color: $secondary;
|
118
|
-
padding: 0 1;
|
119
|
-
}
|
164
|
+
}
|
165
|
+
Rule.-horizontal {
|
166
|
+
height: 1;
|
167
|
+
margin: 0 0;
|
120
168
|
|
121
169
|
}
|
170
|
+
.storage-letter {
|
171
|
+
color: $secondary;
|
172
|
+
padding: 0 1;
|
173
|
+
}
|
174
|
+
|
122
175
|
}
|
123
176
|
#header {
|
124
177
|
width: 100%;
|
@@ -127,7 +180,7 @@ class StorageListSidebar(VerticalScroll):
|
|
127
180
|
width: auto;
|
128
181
|
}
|
129
182
|
|
130
|
-
|
183
|
+
Spinner {
|
131
184
|
width: 5%;
|
132
185
|
}
|
133
186
|
}
|
@@ -136,7 +189,7 @@ class StorageListSidebar(VerticalScroll):
|
|
136
189
|
def compose(self) -> ComposeResult:
|
137
190
|
with Horizontal(id="header"):
|
138
191
|
if self.is_loading:
|
139
|
-
yield
|
192
|
+
yield Spinner()
|
140
193
|
yield GradientWidget(
|
141
194
|
" SOURCERER" if self.is_loading else "🧙SOURCERER",
|
142
195
|
id="left-middle",
|
@@ -150,11 +203,11 @@ class StorageListSidebar(VerticalScroll):
|
|
150
203
|
for storage in storages
|
151
204
|
]
|
152
205
|
storages = sorted(storages, key=lambda x: x.storage.storage)
|
206
|
+
with ScrollVerticalContainerWithNoBindings():
|
207
|
+
for letter, storages_group in groupby(
|
208
|
+
storages, key=lambda x: x.storage.storage[0]
|
209
|
+
):
|
153
210
|
|
154
|
-
for letter, storages_group in groupby(
|
155
|
-
storages, key=lambda x: x.storage.storage[0]
|
156
|
-
):
|
157
|
-
with Container(id=f"group-{letter}", classes="storage-group"):
|
158
211
|
yield Horizontal(
|
159
212
|
Rule(id="rule-left"),
|
160
213
|
Label(letter.upper(), classes="storage-letter"),
|
@@ -163,15 +216,30 @@ class StorageListSidebar(VerticalScroll):
|
|
163
216
|
|
164
217
|
for item in storages_group:
|
165
218
|
yield StorageItem(
|
166
|
-
renderable=STORAGE_ICONS.get(item.storage.provider, "")
|
167
|
-
+ " "
|
168
|
-
+ item.storage.storage,
|
219
|
+
renderable=f'{STORAGE_ICONS.get(item.storage.provider, "")} {item.storage.storage}',
|
169
220
|
storage_name=item.storage.storage,
|
170
221
|
access_credentials_uuid=item.access_credentials_uuid,
|
171
222
|
)
|
172
223
|
|
224
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
225
|
+
try:
|
226
|
+
content = self.query_one(StorageItem)
|
227
|
+
except NoMatches:
|
228
|
+
return self
|
229
|
+
content.focus()
|
230
|
+
return self
|
231
|
+
|
173
232
|
@on(Button.Click)
|
174
233
|
def on_button_click(self, event: Button.Click) -> None:
|
175
234
|
"""Handle button click events to refresh the storage list."""
|
176
235
|
if event.action == "header_click":
|
177
236
|
self.post_message(RefreshStoragesListRequest())
|
237
|
+
|
238
|
+
@on(SelectStorageItem)
|
239
|
+
def on_select_storage_item(self, event: SelectStorageItem) -> None:
|
240
|
+
"""Handle selection of a storage item."""
|
241
|
+
for child in self.query(StorageItem):
|
242
|
+
child.selected = (
|
243
|
+
child.storage_name == event.name
|
244
|
+
and child.access_credentials_uuid == event.access_credentials_uuid
|
245
|
+
)
|