data-sourcerer 0.2.3__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.3.dist-info → data_sourcerer-0.3.0.dist-info}/METADATA +1 -1
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/RECORD +42 -28
- 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 +5 -4
- 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 +187 -77
- 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/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 +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.3.dist-info → data_sourcerer-0.3.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.2.3.dist-info → data_sourcerer-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,7 @@ from sourcerer.presentation.screens.shared.containers import (
|
|
21
21
|
ScrollHorizontalContainerWithNoBindings,
|
22
22
|
ScrollVerticalContainerWithNoBindings,
|
23
23
|
)
|
24
|
+
from sourcerer.presentation.settings import KeyBindings
|
24
25
|
from sourcerer.settings import DIRECTORY_ICON, DOUBLE_CLICK_THRESHOLD, FILE_ICON
|
25
26
|
|
26
27
|
|
@@ -48,7 +49,7 @@ class FileSystemWidget(Widget):
|
|
48
49
|
background: $block-cursor-blurred-background;
|
49
50
|
text-style: $block-cursor-blurred-text-style;
|
50
51
|
}
|
51
|
-
|
52
|
+
|
52
53
|
.folder-name {
|
53
54
|
text-overflow: ellipsis;
|
54
55
|
text-wrap: nowrap;
|
@@ -161,7 +162,7 @@ class FileSystemWidget(Widget):
|
|
161
162
|
Calls the `on_click` method if the pressed key is "enter", which may trigger
|
162
163
|
folder navigation or file opening depending on the widget's context.
|
163
164
|
"""
|
164
|
-
if event.key ==
|
165
|
+
if event.key == KeyBindings.ENTER.value:
|
165
166
|
self.on_file_select()
|
166
167
|
|
167
168
|
def on_file_select(self):
|
@@ -232,11 +233,13 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
232
233
|
|
233
234
|
# Consolidate key binding data
|
234
235
|
BINDINGS: ClassVar[list[BindingType]] = [
|
235
|
-
Binding(
|
236
|
-
Binding(
|
237
|
-
Binding(
|
238
|
-
Binding(
|
239
|
-
Binding(
|
236
|
+
Binding(KeyBindings.ENTER.value, "select_cursor", "Select", show=False),
|
237
|
+
Binding(KeyBindings.ARROW_UP.value, "cursor_up", "Cursor up", show=False),
|
238
|
+
Binding(KeyBindings.ARROW_DOWN.value, "cursor_down", "Cursor down", show=False),
|
239
|
+
Binding(KeyBindings.ARROW_LEFT.value, "cursor_left", "Cursor left", show=False),
|
240
|
+
Binding(
|
241
|
+
KeyBindings.ARROW_RIGHT.value, "cursor_right", "Cursor right", show=False
|
242
|
+
),
|
240
243
|
]
|
241
244
|
|
242
245
|
MAIN_CONTAINER_ID: ClassVar[str] = "dirs_content"
|
@@ -306,9 +309,9 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
306
309
|
)
|
307
310
|
self._focus_first_child(path_listing_container)
|
308
311
|
|
309
|
-
self.path_listing_containers_uuids[
|
310
|
-
|
311
|
-
|
312
|
+
self.path_listing_containers_uuids[
|
313
|
+
str(self.work_dir)
|
314
|
+
] = path_listing_container.id
|
312
315
|
|
313
316
|
def action_cursor_down(self) -> None:
|
314
317
|
"""
|
@@ -626,9 +629,9 @@ class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
|
626
629
|
await self._mount_path_listing_container(path_listing_container)
|
627
630
|
|
628
631
|
if str(folder_path) not in self.path_listing_containers_uuids:
|
629
|
-
self.path_listing_containers_uuids[
|
630
|
-
|
631
|
-
|
632
|
+
self.path_listing_containers_uuids[
|
633
|
+
str(folder_path)
|
634
|
+
] = path_listing_container.id
|
632
635
|
|
633
636
|
@on(FileSystemWidget.Focus)
|
634
637
|
def on_folder_focus(self, event: FileSystemWidget.Focus):
|
@@ -2,19 +2,23 @@ import time
|
|
2
2
|
import traceback
|
3
3
|
from concurrent.futures import ThreadPoolExecutor
|
4
4
|
from pathlib import Path
|
5
|
+
from typing import ClassVar
|
5
6
|
|
7
|
+
from dependency_injector.wiring import Provide
|
6
8
|
from textual import on, work
|
7
9
|
from textual.app import App, ComposeResult
|
8
|
-
from textual.binding import Binding
|
10
|
+
from textual.binding import Binding, BindingType
|
9
11
|
from textual.containers import Horizontal
|
10
12
|
from textual.reactive import reactive
|
11
13
|
from textual.widgets import Footer
|
12
14
|
|
15
|
+
from sourcerer.domain.storage_provider.entities import Storage
|
13
16
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
14
17
|
from sourcerer.infrastructure.storage_provider.exceptions import (
|
15
18
|
ListStorageItemsError,
|
16
19
|
)
|
17
20
|
from sourcerer.infrastructure.utils import generate_uuid
|
21
|
+
from sourcerer.presentation.di_container import DiContainer
|
18
22
|
from sourcerer.presentation.screens.critical_error.main import CriticalErrorScreen
|
19
23
|
from sourcerer.presentation.screens.file_system_finder.main import (
|
20
24
|
FileSystemNavigationModal,
|
@@ -55,6 +59,8 @@ from sourcerer.presentation.screens.storage_action_progress.main import (
|
|
55
59
|
StorageActionProgressScreen,
|
56
60
|
UploadKey,
|
57
61
|
)
|
62
|
+
from sourcerer.presentation.screens.storages_list.main import StoragesListScreen
|
63
|
+
from sourcerer.presentation.settings import KeyBindings
|
58
64
|
from sourcerer.presentation.themes.github_dark import github_dark_theme
|
59
65
|
from sourcerer.presentation.utils import (
|
60
66
|
get_provider_service_by_access_credentials,
|
@@ -93,12 +99,28 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
93
99
|
"""
|
94
100
|
|
95
101
|
CSS_PATH = "styles.tcss"
|
96
|
-
BINDINGS = [
|
102
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
103
|
+
Binding("ctrl+r", "registrations", "Registrations list"),
|
104
|
+
Binding("ctrl+s", "storages", "Storages list"),
|
105
|
+
Binding(
|
106
|
+
KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
|
107
|
+
),
|
108
|
+
Binding(
|
109
|
+
KeyBindings.ARROW_RIGHT.value, "focus_content", "Focus content", show=False
|
110
|
+
),
|
111
|
+
]
|
97
112
|
is_storage_list_loading = reactive(False, recompose=True)
|
98
113
|
|
99
|
-
def __init__(
|
114
|
+
def __init__(
|
115
|
+
self,
|
116
|
+
credentials_service: CredentialsService = Provide[
|
117
|
+
DiContainer.credentials_service
|
118
|
+
],
|
119
|
+
*args,
|
120
|
+
**kwargs,
|
121
|
+
):
|
100
122
|
super().__init__(*args, **kwargs)
|
101
|
-
self.credentials_service =
|
123
|
+
self.credentials_service = credentials_service
|
102
124
|
self.storage_list_sidebar = StorageListSidebar(id="storage_list_sidebar")
|
103
125
|
self.storage_content = StorageContentContainer(id="storage_content_container")
|
104
126
|
self.load_percentage = 0
|
@@ -130,6 +152,18 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
130
152
|
self.theme = "github-dark"
|
131
153
|
self.init_storages_list()
|
132
154
|
|
155
|
+
def action_focus_content(self):
|
156
|
+
"""
|
157
|
+
Focuses the storage content container.
|
158
|
+
"""
|
159
|
+
self.storage_content.focus()
|
160
|
+
|
161
|
+
def action_focus_sidebar(self):
|
162
|
+
"""
|
163
|
+
Focuses the storage list sidebar.
|
164
|
+
"""
|
165
|
+
self.storage_list_sidebar.focus()
|
166
|
+
|
133
167
|
def action_registrations(self):
|
134
168
|
"""
|
135
169
|
Opens the provider credentials list screen and refreshes the storage list.
|
@@ -142,6 +176,9 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
142
176
|
"""
|
143
177
|
self.app.push_screen(ProviderCredsListScreen(), callback=self.refresh_storages)
|
144
178
|
|
179
|
+
def action_storages(self):
|
180
|
+
self.app.push_screen(StoragesListScreen(), callback=self.refresh_storages)
|
181
|
+
|
145
182
|
def refresh_storages(self, *args, **kwargs):
|
146
183
|
"""
|
147
184
|
Refreshes the storage list by clearing the current storages and
|
@@ -206,7 +243,11 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
206
243
|
5. Notify the user if an error occurs during the retrieval process.
|
207
244
|
"""
|
208
245
|
self.refresh_storage_content(
|
209
|
-
event.access_credentials_uuid,
|
246
|
+
event.access_credentials_uuid,
|
247
|
+
event.name,
|
248
|
+
event.path,
|
249
|
+
event.prefix,
|
250
|
+
event.focus_content,
|
210
251
|
)
|
211
252
|
|
212
253
|
@on(UploadRequest)
|
@@ -381,7 +422,12 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
381
422
|
self.uncheck_files_request(UncheckFilesRequest(keys=[]))
|
382
423
|
|
383
424
|
def refresh_storage_content(
|
384
|
-
self,
|
425
|
+
self,
|
426
|
+
access_credentials_uuid,
|
427
|
+
storage_name,
|
428
|
+
path,
|
429
|
+
prefix=None,
|
430
|
+
focus_content=False,
|
385
431
|
):
|
386
432
|
"""
|
387
433
|
Refreshes the storage content display with items from the specified storage path.
|
@@ -407,6 +453,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
407
453
|
self.storage_content.storage_content = None
|
408
454
|
self.storage_content.selected_files = set()
|
409
455
|
self.storage_content.selected_files_n = 0
|
456
|
+
self.storage_content.focus_content = focus_content
|
410
457
|
|
411
458
|
provider_service = get_provider_service_by_access_uuid(
|
412
459
|
access_credentials_uuid, self.credentials_service
|
@@ -421,7 +468,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
421
468
|
**params
|
422
469
|
)
|
423
470
|
except ListStorageItemsError as e:
|
424
|
-
self.notify_error(f"""Could not extract storage content \n{
|
471
|
+
self.notify_error(f"""Could not extract storage content \n{e}""")
|
425
472
|
|
426
473
|
def _upload_file(
|
427
474
|
self,
|
@@ -509,7 +556,15 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
509
556
|
|
510
557
|
try:
|
511
558
|
storages = provider_service.list_storages()
|
512
|
-
|
559
|
+
storage_names = [storage.storage for storage in storages]
|
560
|
+
registered_storages = [
|
561
|
+
Storage(credentials.provider, storage.name, storage.created_at)
|
562
|
+
for storage in credentials.storages
|
563
|
+
if storage.name not in storage_names
|
564
|
+
]
|
565
|
+
self.storage_list_sidebar.storages[credentials.uuid] = (
|
566
|
+
storages + registered_storages
|
567
|
+
)
|
513
568
|
self.storage_list_sidebar.last_update_timestamp = time.time()
|
514
569
|
except Exception:
|
515
570
|
self.notify_error(f"Could not get storages list for {credentials.name}!")
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import time
|
2
|
+
from typing import ClassVar
|
2
3
|
|
3
4
|
from textual.events import MouseMove, MouseUp
|
4
5
|
|
@@ -28,7 +29,7 @@ class ResizeContainersWatcherMixin:
|
|
28
29
|
and is inherited from textual App.
|
29
30
|
"""
|
30
31
|
|
31
|
-
required_methods = ["query_one", "refresh"]
|
32
|
+
required_methods: ClassVar[list[str]] = ["query_one", "refresh"]
|
32
33
|
|
33
34
|
def __init_subclass__(cls, **kwargs):
|
34
35
|
super().__init_subclass__(**kwargs)
|
@@ -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
|
|
@@ -340,6 +415,16 @@ class StorageContentContainer(Vertical):
|
|
340
415
|
] = reactive(None, recompose=True)
|
341
416
|
selected_files: reactive[set] = reactive(set(), recompose=False)
|
342
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
|
+
]
|
343
428
|
|
344
429
|
DEFAULT_CSS = """
|
345
430
|
|
@@ -375,7 +460,7 @@ class StorageContentContainer(Vertical):
|
|
375
460
|
width: 100%;
|
376
461
|
height: auto;
|
377
462
|
border-bottom: solid $secondary;
|
378
|
-
margin: 1 0;
|
463
|
+
margin: 1 0 0 0;
|
379
464
|
|
380
465
|
PathSelector {
|
381
466
|
&.primary_color {
|
@@ -524,13 +609,28 @@ class StorageContentContainer(Vertical):
|
|
524
609
|
yield FileMetaLabel("Size", classes="file_size")
|
525
610
|
yield FileMetaLabel("Date modified", classes="file_date")
|
526
611
|
yield FileMetaLabel("Preview", classes="preview")
|
527
|
-
with
|
612
|
+
with ScrollVerticalContainerWithNoBindings(id="content", can_focus=False):
|
528
613
|
for folder in self.storage_content.folders:
|
529
614
|
yield FolderItem(
|
530
|
-
self.storage,
|
615
|
+
self.storage,
|
616
|
+
self.access_credentials_uuid,
|
617
|
+
self.path,
|
618
|
+
folder,
|
619
|
+
self.focus_content,
|
531
620
|
)
|
532
621
|
for file in self.storage_content.files:
|
533
|
-
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
|
534
634
|
|
535
635
|
@on(Input.Submitted)
|
536
636
|
def on_input_submitted(self, event: Input.Submitted):
|
@@ -545,19 +645,6 @@ class StorageContentContainer(Vertical):
|
|
545
645
|
"""
|
546
646
|
self.apply_search_prefix(event.value)
|
547
647
|
|
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
648
|
@on(FileItem.Preview)
|
562
649
|
def on_file_item_preview(self, event: FileItem.Preview):
|
563
650
|
"""
|
@@ -711,5 +798,28 @@ class StorageContentContainer(Vertical):
|
|
711
798
|
self.path,
|
712
799
|
self.access_credentials_uuid,
|
713
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,
|
714
824
|
)
|
715
825
|
)
|