data-sourcerer 0.2.3__py3-none-any.whl → 0.4.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.3.dist-info → data_sourcerer-0.4.0.dist-info}/METADATA +3 -1
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/RECORD +50 -34
- 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/domain/storage_provider/entities.py +1 -1
- 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/azure.py +1 -3
- sourcerer/infrastructure/storage_provider/services/gcp.py +2 -3
- sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
- sourcerer/infrastructure/utils.py +2 -1
- sourcerer/presentation/di_container.py +15 -0
- sourcerer/presentation/screens/file_system_finder/main.py +5 -10
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +16 -13
- sourcerer/presentation/screens/main/main.py +89 -9
- sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
- 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 +197 -80
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +99 -31
- sourcerer/presentation/screens/preview_content/main.py +216 -17
- sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
- sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +38 -13
- sourcerer/presentation/screens/provider_creds_registration/main.py +10 -7
- sourcerer/presentation/screens/shared/modal_screens.py +37 -0
- sourcerer/presentation/screens/shared/widgets/spinner.py +57 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +3 -5
- sourcerer/presentation/screens/storages_list/__init__.py +0 -0
- sourcerer/presentation/screens/storages_list/main.py +184 -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/settings.py +2 -0
- sourcerer/presentation/screens/shared/widgets/loader.py +0 -31
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,18 +7,21 @@ 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
|
|
15
|
+
import humanize
|
13
16
|
from textual import events, on
|
14
17
|
from textual.app import ComposeResult
|
18
|
+
from textual.binding import Binding, BindingType
|
15
19
|
from textual.containers import (
|
16
20
|
Center,
|
17
21
|
Container,
|
18
22
|
Horizontal,
|
19
23
|
Middle,
|
20
24
|
Vertical,
|
21
|
-
VerticalScroll,
|
22
25
|
)
|
23
26
|
from textual.css.query import NoMatches
|
24
27
|
from textual.message import Message
|
@@ -38,8 +41,11 @@ from sourcerer.presentation.screens.main.messages.uncheck_files_request import (
|
|
38
41
|
UncheckFilesRequest,
|
39
42
|
)
|
40
43
|
from sourcerer.presentation.screens.main.messages.upload_request import UploadRequest
|
44
|
+
from sourcerer.presentation.screens.shared.containers import (
|
45
|
+
ScrollVerticalContainerWithNoBindings,
|
46
|
+
)
|
41
47
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
42
|
-
from sourcerer.presentation.settings import NO_DATA_LOGO
|
48
|
+
from sourcerer.presentation.settings import NO_DATA_LOGO, KeyBindings
|
43
49
|
from sourcerer.settings import (
|
44
50
|
DIRECTORY_ICON,
|
45
51
|
DOWNLOAD_ICON,
|
@@ -91,6 +97,10 @@ class ActionType(Enum):
|
|
91
97
|
return action_map[action_str]
|
92
98
|
|
93
99
|
|
100
|
+
class UnfocusableCheckbox(Checkbox):
|
101
|
+
can_focus = False
|
102
|
+
|
103
|
+
|
94
104
|
class FileMetaLabel(Static):
|
95
105
|
"""Widget for displaying file metadata information.
|
96
106
|
|
@@ -120,6 +130,16 @@ class PathSelector(Label):
|
|
120
130
|
access_credentials_uuid: UUID of the access credentials being used
|
121
131
|
"""
|
122
132
|
|
133
|
+
can_focus = True
|
134
|
+
|
135
|
+
DEFAULT_CSS = """
|
136
|
+
PathSelector {
|
137
|
+
&:focus {
|
138
|
+
background: $secondary-lighten-2;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
"""
|
142
|
+
|
123
143
|
def __init__(self, storage, path, access_credentials_uuid, *args, **kwargs):
|
124
144
|
super().__init__(*args, **kwargs)
|
125
145
|
self.storage = storage
|
@@ -128,31 +148,93 @@ class PathSelector(Label):
|
|
128
148
|
|
129
149
|
def on_click(self, _: events.Click) -> None:
|
130
150
|
"""Handle click events to navigate to the selected path."""
|
151
|
+
self._select()
|
152
|
+
|
153
|
+
def on_key(self, event: events.Key) -> None:
|
154
|
+
"""Handle key events to navigate to the selected path."""
|
155
|
+
if event.key == KeyBindings.ENTER.value:
|
156
|
+
self._select()
|
157
|
+
|
158
|
+
def _select(self):
|
159
|
+
"""Select the current path."""
|
131
160
|
self.post_message(
|
132
|
-
SelectStorageItem(
|
161
|
+
SelectStorageItem(
|
162
|
+
self.storage,
|
163
|
+
self.path,
|
164
|
+
self.access_credentials_uuid,
|
165
|
+
focus_content=True,
|
166
|
+
)
|
133
167
|
)
|
134
168
|
|
135
169
|
|
136
|
-
class
|
170
|
+
class StorageContentItem(Horizontal):
|
171
|
+
DEFAULT_CSS = """
|
172
|
+
StorageContentItem.active {
|
173
|
+
background: $secondary;
|
174
|
+
color: $panel;
|
175
|
+
}
|
176
|
+
StorageContentItem:focus {
|
177
|
+
background: $secondary-lighten-2;
|
178
|
+
color: $panel;
|
179
|
+
}
|
180
|
+
"""
|
181
|
+
|
182
|
+
can_focus = True
|
183
|
+
|
184
|
+
def __init__(self, focus_first: bool, *args, **kwargs):
|
185
|
+
"""Initialize the storage content widget."""
|
186
|
+
super().__init__(*args, **kwargs)
|
187
|
+
self.focus_first = focus_first
|
188
|
+
|
189
|
+
def on_mount(self) -> None:
|
190
|
+
"""Handle the mounting of the widget."""
|
191
|
+
if self.focus_first and self.first_child:
|
192
|
+
self.focus()
|
193
|
+
|
194
|
+
@abstractmethod
|
195
|
+
def _select(self, widget=None):
|
196
|
+
raise NotImplementedError
|
197
|
+
|
198
|
+
def on_click(self, event: events.Click) -> None:
|
199
|
+
"""Handle click events to navigate into the folder."""
|
200
|
+
self._select(event.widget)
|
201
|
+
|
202
|
+
def on_key(self, event: events.Key) -> None:
|
203
|
+
"""Handle key events to navigate into the folder."""
|
204
|
+
if event.key == KeyBindings.ARROW_UP.value:
|
205
|
+
if self.first_child:
|
206
|
+
self.parent.children[-1].focus() # type: ignore
|
207
|
+
return
|
208
|
+
self.screen.focus_previous()
|
209
|
+
if event.key == KeyBindings.ARROW_DOWN.value:
|
210
|
+
if self.last_child:
|
211
|
+
self.parent.children[0].focus() # type: ignore
|
212
|
+
return
|
213
|
+
self.screen.focus_next()
|
214
|
+
|
215
|
+
@on(events.Enter)
|
216
|
+
@on(events.Leave)
|
217
|
+
def on_enter(self, _: events.Enter):
|
218
|
+
with contextlib.suppress(Exception):
|
219
|
+
self.set_class(self.is_mouse_over, "active")
|
220
|
+
|
221
|
+
|
222
|
+
class FolderItem(StorageContentItem):
|
137
223
|
"""Widget for displaying and interacting with folder items.
|
138
224
|
|
139
225
|
This widget represents a folder in the storage content view, allowing
|
140
226
|
navigation into the folder and visual feedback on hover/selection.
|
141
227
|
"""
|
142
228
|
|
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
229
|
def __init__(
|
155
|
-
self,
|
230
|
+
self,
|
231
|
+
storage,
|
232
|
+
access_credentials_uuid,
|
233
|
+
parent_path,
|
234
|
+
folder,
|
235
|
+
focus_first,
|
236
|
+
*args,
|
237
|
+
**kwargs,
|
156
238
|
):
|
157
239
|
"""Initialize a folder item widget.
|
158
240
|
|
@@ -162,7 +244,7 @@ class FolderItem(Horizontal):
|
|
162
244
|
parent_path: The parent path of the folder
|
163
245
|
folder: The folder name
|
164
246
|
"""
|
165
|
-
super().__init__(*args, **kwargs)
|
247
|
+
super().__init__(focus_first, *args, **kwargs)
|
166
248
|
self.storage = storage
|
167
249
|
self.access_credentials_uuid = access_credentials_uuid
|
168
250
|
self.parent_path = parent_path
|
@@ -172,26 +254,29 @@ class FolderItem(Horizontal):
|
|
172
254
|
"""Compose the folder item layout with folder name and icon."""
|
173
255
|
yield Label(f"{DIRECTORY_ICON}{self.folder.key}", markup=False)
|
174
256
|
|
175
|
-
def
|
176
|
-
"""
|
257
|
+
def _select(self, widget=None):
|
258
|
+
"""Select the folder."""
|
177
259
|
path = self.folder.key
|
178
260
|
if self.parent_path:
|
179
261
|
path = self.parent_path.strip("/") + "/" + path
|
180
262
|
|
181
263
|
self.post_message(
|
182
|
-
SelectStorageItem(
|
264
|
+
SelectStorageItem(
|
265
|
+
self.storage, path, self.access_credentials_uuid, focus_content=True
|
266
|
+
)
|
183
267
|
)
|
184
268
|
|
185
|
-
def
|
186
|
-
"""Handle
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
269
|
+
def on_key(self, event: events.Key) -> None:
|
270
|
+
"""Handle key events to navigate into the folder."""
|
271
|
+
if event.key in (KeyBindings.ARROW_UP.value, KeyBindings.ARROW_DOWN.value):
|
272
|
+
event.prevent_default()
|
273
|
+
if event.key == KeyBindings.ENTER.value:
|
274
|
+
self._select()
|
275
|
+
return
|
276
|
+
super().on_key(event)
|
192
277
|
|
193
278
|
|
194
|
-
class FileItem(
|
279
|
+
class FileItem(StorageContentItem):
|
195
280
|
"""Widget for displaying and interacting with file items.
|
196
281
|
|
197
282
|
This widget represents a file in the storage content view, allowing
|
@@ -199,17 +284,10 @@ class FileItem(Horizontal):
|
|
199
284
|
"""
|
200
285
|
|
201
286
|
DEFAULT_CSS = """
|
202
|
-
FileItem {
|
203
|
-
margin-bottom: 1;
|
204
|
-
}
|
205
|
-
FileItem.active {
|
206
|
-
background: $secondary;
|
207
|
-
color: $panel;
|
208
|
-
}
|
209
287
|
.file_size {
|
210
288
|
color: $primary
|
211
289
|
}
|
212
|
-
|
290
|
+
UnfocusableCheckbox {
|
213
291
|
border: none;
|
214
292
|
padding: 0 0;
|
215
293
|
display: none;
|
@@ -219,7 +297,6 @@ class FileItem(Horizontal):
|
|
219
297
|
}
|
220
298
|
}
|
221
299
|
"""
|
222
|
-
can_focus = False
|
223
300
|
|
224
301
|
@dataclass
|
225
302
|
class Selected(Message):
|
@@ -232,6 +309,7 @@ class FileItem(Horizontal):
|
|
232
309
|
"""Message sent when a file preview is selected."""
|
233
310
|
|
234
311
|
name: str
|
312
|
+
size: int
|
235
313
|
|
236
314
|
@dataclass
|
237
315
|
class Unselect(Message):
|
@@ -239,11 +317,7 @@ class FileItem(Horizontal):
|
|
239
317
|
|
240
318
|
name: str
|
241
319
|
|
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):
|
320
|
+
def __init__(self, storage, parent_path, file, focus_first, *args, **kwargs):
|
247
321
|
"""Initialize a file item widget.
|
248
322
|
|
249
323
|
Args:
|
@@ -251,59 +325,64 @@ class FileItem(Horizontal):
|
|
251
325
|
parent_path: The parent path of the file
|
252
326
|
file: The file name
|
253
327
|
"""
|
254
|
-
super().__init__(*args, **kwargs)
|
328
|
+
super().__init__(focus_first, *args, **kwargs)
|
255
329
|
self.storage = storage
|
256
330
|
self.parent_path = parent_path
|
257
331
|
self.file = file
|
258
332
|
|
259
333
|
def compose(self):
|
260
|
-
yield
|
334
|
+
yield UnfocusableCheckbox()
|
261
335
|
yield FileMetaLabel(
|
262
336
|
f"{FILE_ICON} {self.file.key}", classes="file_name", markup=False
|
263
337
|
)
|
264
|
-
yield FileMetaLabel(
|
338
|
+
yield FileMetaLabel(
|
339
|
+
f"{humanize.naturalsize(self.file.size)}", classes="file_size", markup=False
|
340
|
+
)
|
265
341
|
yield FileMetaLabel(
|
266
342
|
str(self.file.date_modified), classes="file_date", markup=False
|
267
343
|
)
|
268
344
|
if self.file.is_text:
|
269
345
|
yield Button(f"{PREVIEW_ICON}", name="preview", classes="download")
|
270
346
|
|
271
|
-
def
|
272
|
-
"""Handle
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
347
|
+
def on_key(self, event: events.Key) -> None:
|
348
|
+
"""Handle key events to toggle file selection."""
|
349
|
+
if event.key in (KeyBindings.ARROW_UP.value, KeyBindings.ARROW_DOWN.value):
|
350
|
+
event.prevent_default()
|
351
|
+
if event.key == KeyBindings.ENTER.value:
|
352
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
353
|
+
checkbox.value = not checkbox.value
|
354
|
+
if checkbox.value:
|
355
|
+
self.post_message(self.Selected(self.file.key))
|
356
|
+
else:
|
357
|
+
self.post_message(self.Unselect(self.file.key))
|
358
|
+
return
|
359
|
+
super().on_key(event)
|
278
360
|
|
279
|
-
def
|
280
|
-
"""Handle click events to toggle file selection."""
|
361
|
+
def _select(self, widget=None):
|
281
362
|
preview_button = None
|
282
363
|
with contextlib.suppress(NoMatches):
|
283
364
|
preview_button = self.query_one(Button)
|
284
365
|
|
285
|
-
if
|
286
|
-
self.post_message(self.Preview(self.file.key))
|
366
|
+
if widget is preview_button:
|
367
|
+
self.post_message(self.Preview(self.file.key, self.file.size))
|
287
368
|
return
|
288
369
|
|
289
|
-
checkbox = self.query_one(
|
290
|
-
if
|
370
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
371
|
+
if widget is not checkbox:
|
291
372
|
checkbox.value = not checkbox.value
|
292
373
|
if checkbox.value:
|
293
374
|
self.post_message(self.Selected(self.file.key))
|
294
375
|
else:
|
295
376
|
self.post_message(self.Unselect(self.file.key))
|
296
|
-
event.prevent_default()
|
297
|
-
event.stop()
|
298
377
|
|
299
378
|
def uncheck(self):
|
300
379
|
"""Uncheck the file's checkbox."""
|
301
|
-
checkbox = self.query_one(
|
380
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
302
381
|
checkbox.value = False
|
303
382
|
|
304
383
|
def check(self):
|
305
384
|
"""Check the file's checkbox."""
|
306
|
-
checkbox = self.query_one(
|
385
|
+
checkbox = self.query_one(UnfocusableCheckbox)
|
307
386
|
checkbox.value = True
|
308
387
|
|
309
388
|
|
@@ -340,6 +419,16 @@ class StorageContentContainer(Vertical):
|
|
340
419
|
] = reactive(None, recompose=True)
|
341
420
|
selected_files: reactive[set] = reactive(set(), recompose=False)
|
342
421
|
selected_files_n: reactive[int] = reactive(0, recompose=False)
|
422
|
+
focus_content: reactive[bool] = reactive(False, recompose=False)
|
423
|
+
|
424
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
425
|
+
Binding(
|
426
|
+
f"{KeyBindings.CTRL.value}+{KeyBindings.BACKSPACE.value}",
|
427
|
+
"back_to_prev_path",
|
428
|
+
"Navigate back to the previous path",
|
429
|
+
show=True,
|
430
|
+
),
|
431
|
+
]
|
343
432
|
|
344
433
|
DEFAULT_CSS = """
|
345
434
|
|
@@ -375,7 +464,7 @@ class StorageContentContainer(Vertical):
|
|
375
464
|
width: 100%;
|
376
465
|
height: auto;
|
377
466
|
border-bottom: solid $secondary;
|
378
|
-
margin: 1 0;
|
467
|
+
margin: 1 0 0 0;
|
379
468
|
|
380
469
|
PathSelector {
|
381
470
|
&.primary_color {
|
@@ -476,6 +565,8 @@ class StorageContentContainer(Vertical):
|
|
476
565
|
}
|
477
566
|
"""
|
478
567
|
|
568
|
+
search_input_id: ClassVar[str] = "search_input"
|
569
|
+
|
479
570
|
def compose(self) -> ComposeResult:
|
480
571
|
if not self.storage:
|
481
572
|
return
|
@@ -499,7 +590,7 @@ class StorageContentContainer(Vertical):
|
|
499
590
|
with Horizontal():
|
500
591
|
yield Label("Search:")
|
501
592
|
yield Input(
|
502
|
-
id=
|
593
|
+
id=self.search_input_id,
|
503
594
|
placeholder="input path prefix here...",
|
504
595
|
value=self.search_prefix,
|
505
596
|
)
|
@@ -524,13 +615,28 @@ class StorageContentContainer(Vertical):
|
|
524
615
|
yield FileMetaLabel("Size", classes="file_size")
|
525
616
|
yield FileMetaLabel("Date modified", classes="file_date")
|
526
617
|
yield FileMetaLabel("Preview", classes="preview")
|
527
|
-
with
|
618
|
+
with ScrollVerticalContainerWithNoBindings(id="content", can_focus=False):
|
528
619
|
for folder in self.storage_content.folders:
|
529
620
|
yield FolderItem(
|
530
|
-
self.storage,
|
621
|
+
self.storage,
|
622
|
+
self.access_credentials_uuid,
|
623
|
+
self.path,
|
624
|
+
folder,
|
625
|
+
self.focus_content,
|
531
626
|
)
|
532
627
|
for file in self.storage_content.files:
|
533
|
-
yield FileItem(
|
628
|
+
yield FileItem(
|
629
|
+
self.storage, self.path, file, self.focus_content, id=file.uuid
|
630
|
+
)
|
631
|
+
|
632
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
633
|
+
try:
|
634
|
+
content = self.query_one(ScrollVerticalContainerWithNoBindings)
|
635
|
+
except NoMatches:
|
636
|
+
return self
|
637
|
+
if len(content.children) > 0:
|
638
|
+
content.children[0].focus()
|
639
|
+
return self
|
534
640
|
|
535
641
|
@on(Input.Submitted)
|
536
642
|
def on_input_submitted(self, event: Input.Submitted):
|
@@ -545,19 +651,6 @@ class StorageContentContainer(Vertical):
|
|
545
651
|
"""
|
546
652
|
self.apply_search_prefix(event.value)
|
547
653
|
|
548
|
-
@on(Input.Blurred)
|
549
|
-
def on_input_blurred(self, event: Input.Blurred):
|
550
|
-
"""
|
551
|
-
Handle input blur events to apply the search prefix.
|
552
|
-
|
553
|
-
This method is triggered when the input field loses focus and applies
|
554
|
-
the search prefix to the current storage content.
|
555
|
-
|
556
|
-
Args:
|
557
|
-
event (Input.Blurred): The blur event containing the input value
|
558
|
-
"""
|
559
|
-
self.apply_search_prefix(event.value)
|
560
|
-
|
561
654
|
@on(FileItem.Preview)
|
562
655
|
def on_file_item_preview(self, event: FileItem.Preview):
|
563
656
|
"""
|
@@ -576,6 +669,7 @@ class StorageContentContainer(Vertical):
|
|
576
669
|
self.storage,
|
577
670
|
self.access_credentials_uuid,
|
578
671
|
os.path.join(self.path, event.name) if self.path else event.name,
|
672
|
+
event.size,
|
579
673
|
)
|
580
674
|
)
|
581
675
|
|
@@ -711,5 +805,28 @@ class StorageContentContainer(Vertical):
|
|
711
805
|
self.path,
|
712
806
|
self.access_credentials_uuid,
|
713
807
|
value,
|
808
|
+
focus_content=True,
|
809
|
+
)
|
810
|
+
)
|
811
|
+
|
812
|
+
def action_back_to_prev_path(self):
|
813
|
+
"""
|
814
|
+
Navigate back to the previous path in the storage content.
|
815
|
+
|
816
|
+
This method updates the path to the parent directory and triggers a
|
817
|
+
SelectStorageItem message to refresh the storage content with the new path.
|
818
|
+
"""
|
819
|
+
if not self.storage:
|
820
|
+
return
|
821
|
+
if not self.path:
|
822
|
+
return
|
823
|
+
path_parents = [i for i in self.path.split("/")[:-1] if i]
|
824
|
+
prev_path = "/".join(path_parents)
|
825
|
+
self.post_message(
|
826
|
+
SelectStorageItem(
|
827
|
+
self.storage,
|
828
|
+
prev_path,
|
829
|
+
self.access_credentials_uuid,
|
830
|
+
focus_content=True,
|
714
831
|
)
|
715
832
|
)
|
@@ -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
|
+
)
|