data-sourcerer 0.3.0__py3-none-any.whl → 0.5.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.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/METADATA +11 -8
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/RECORD +55 -34
- sourcerer/__init__.py +3 -1
- sourcerer/domain/package_meta/__init__.py +0 -0
- sourcerer/domain/package_meta/entities.py +9 -0
- sourcerer/domain/package_meta/services.py +9 -0
- sourcerer/domain/settings/__init__.py +0 -0
- sourcerer/domain/settings/entities.py +11 -0
- sourcerer/domain/settings/repositories.py +20 -0
- sourcerer/domain/settings/services.py +19 -0
- sourcerer/domain/storage_provider/entities.py +1 -1
- sourcerer/infrastructure/db/models.py +23 -0
- sourcerer/infrastructure/package_meta/__init__.py +0 -0
- sourcerer/infrastructure/package_meta/services.py +26 -0
- sourcerer/infrastructure/settings/__init__.py +0 -0
- sourcerer/infrastructure/settings/repositories.py +59 -0
- sourcerer/infrastructure/settings/services.py +16 -0
- sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
- sourcerer/infrastructure/storage_provider/services/gcp.py +1 -3
- sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
- sourcerer/infrastructure/utils.py +1 -0
- sourcerer/presentation/di_container.py +13 -0
- sourcerer/presentation/screens/about/__init__.py +0 -0
- sourcerer/presentation/screens/about/main.py +60 -0
- sourcerer/presentation/screens/about/styles.tcss +32 -0
- sourcerer/presentation/screens/file_system_finder/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/main.py +3 -9
- sourcerer/presentation/screens/main/main.py +116 -8
- sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
- sourcerer/presentation/screens/main/styles.tcss +13 -4
- sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +102 -18
- sourcerer/presentation/screens/preview_content/main.py +202 -15
- 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 +23 -9
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +9 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
- sourcerer/presentation/screens/settings/__init__.py +0 -0
- sourcerer/presentation/screens/settings/main.py +70 -0
- sourcerer/presentation/screens/settings/styles.tcss +44 -0
- sourcerer/presentation/screens/shared/modal_screens.py +37 -0
- sourcerer/presentation/screens/shared/widgets/button.py +11 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -3
- sourcerer/presentation/screens/storage_action_progress/main.py +1 -2
- sourcerer/presentation/screens/storages_list/main.py +24 -9
- sourcerer/presentation/screens/storages_list/styles.tcss +7 -0
- sourcerer/presentation/screens/storages_registration/main.py +3 -3
- sourcerer/presentation/settings.py +1 -0
- sourcerer/presentation/utils.py +1 -0
- sourcerer/settings.py +2 -0
- sourcerer/utils.py +19 -1
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
AboutScreen {
|
2
|
+
align: center middle;
|
3
|
+
content-align: center top;
|
4
|
+
|
5
|
+
& > Container {
|
6
|
+
padding: 1 2 0 2;
|
7
|
+
margin: 0 0;
|
8
|
+
width: 50;
|
9
|
+
height: 10;
|
10
|
+
border: solid $border;
|
11
|
+
|
12
|
+
& > Static {
|
13
|
+
text-align: center;
|
14
|
+
text-wrap: wrap;
|
15
|
+
}
|
16
|
+
|
17
|
+
& > Horizontal#controls {
|
18
|
+
padding-top: 1;
|
19
|
+
align: center bottom;
|
20
|
+
|
21
|
+
& > Button {
|
22
|
+
color: $border;
|
23
|
+
border: none;
|
24
|
+
|
25
|
+
& > :focus {
|
26
|
+
color: $border;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
}
|
File without changes
|
@@ -1,16 +1,13 @@
|
|
1
1
|
from collections.abc import Callable
|
2
2
|
from dataclasses import dataclass
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import ClassVar
|
5
4
|
|
6
5
|
from dependency_injector.wiring import Provide
|
7
6
|
from textual import on
|
8
7
|
from textual.app import ComposeResult
|
9
|
-
from textual.binding import Binding, BindingType
|
10
8
|
from textual.containers import Container, Horizontal
|
11
9
|
from textual.css.query import NoMatches
|
12
10
|
from textual.reactive import reactive
|
13
|
-
from textual.screen import ModalScreen
|
14
11
|
from textual.widgets import Static
|
15
12
|
|
16
13
|
from sourcerer.infrastructure.file_system.services import FileSystemService
|
@@ -18,6 +15,7 @@ from sourcerer.presentation.di_container import DiContainer
|
|
18
15
|
from sourcerer.presentation.screens.file_system_finder.widgets.file_system_navigator import (
|
19
16
|
FileSystemNavigator,
|
20
17
|
)
|
18
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
21
19
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
22
20
|
|
23
21
|
|
@@ -27,14 +25,10 @@ class FileSystemSelectionValidationRule:
|
|
27
25
|
error_message: str
|
28
26
|
|
29
27
|
|
30
|
-
class FileSystemNavigationModal(
|
28
|
+
class FileSystemNavigationModal(ExitBoundModalScreen):
|
31
29
|
CONTAINER_ID = "file_system_view_container"
|
32
30
|
CSS_PATH = "styles.tcss"
|
33
31
|
|
34
|
-
BINDINGS: ClassVar[list[BindingType]] = [
|
35
|
-
Binding("escape", "app.pop_screen", "Pop screen"),
|
36
|
-
]
|
37
|
-
|
38
32
|
active_path: reactive[Path] = reactive(Path())
|
39
33
|
|
40
34
|
def __init__(
|
@@ -126,7 +120,7 @@ class FileSystemNavigationModal(ModalScreen):
|
|
126
120
|
event (Button.Click): The event containing the button that was clicked.
|
127
121
|
"""
|
128
122
|
if event.action == "close":
|
129
|
-
self.
|
123
|
+
self.action_cancel_screen()
|
130
124
|
else:
|
131
125
|
self.on_apply()
|
132
126
|
|
@@ -1,24 +1,32 @@
|
|
1
|
+
import contextlib
|
1
2
|
import time
|
2
3
|
import traceback
|
4
|
+
from collections.abc import Iterable
|
3
5
|
from concurrent.futures import ThreadPoolExecutor
|
4
6
|
from pathlib import Path
|
5
7
|
from typing import ClassVar
|
6
8
|
|
7
9
|
from dependency_injector.wiring import Provide
|
8
10
|
from textual import on, work
|
9
|
-
from textual.app import App, ComposeResult
|
11
|
+
from textual.app import App, ComposeResult, SystemCommand
|
10
12
|
from textual.binding import Binding, BindingType
|
11
13
|
from textual.containers import Horizontal
|
14
|
+
from textual.css.query import NoMatches
|
12
15
|
from textual.reactive import reactive
|
16
|
+
from textual.screen import Screen
|
13
17
|
from textual.widgets import Footer
|
14
18
|
|
19
|
+
from sourcerer.domain.package_meta.services import BasePackageMetaService
|
20
|
+
from sourcerer.domain.settings.entities import SettingsFields
|
15
21
|
from sourcerer.domain.storage_provider.entities import Storage
|
16
22
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
23
|
+
from sourcerer.infrastructure.settings.services import SettingsService
|
17
24
|
from sourcerer.infrastructure.storage_provider.exceptions import (
|
18
25
|
ListStorageItemsError,
|
19
26
|
)
|
20
27
|
from sourcerer.infrastructure.utils import generate_uuid
|
21
28
|
from sourcerer.presentation.di_container import DiContainer
|
29
|
+
from sourcerer.presentation.screens.about.main import AboutScreen
|
22
30
|
from sourcerer.presentation.screens.critical_error.main import CriticalErrorScreen
|
23
31
|
from sourcerer.presentation.screens.file_system_finder.main import (
|
24
32
|
FileSystemNavigationModal,
|
@@ -53,6 +61,7 @@ from sourcerer.presentation.screens.preview_content.main import PreviewContentSc
|
|
53
61
|
from sourcerer.presentation.screens.provider_creds_list.main import (
|
54
62
|
ProviderCredsListScreen,
|
55
63
|
)
|
64
|
+
from sourcerer.presentation.screens.settings.main import SettingsScreen
|
56
65
|
from sourcerer.presentation.screens.storage_action_progress.main import (
|
57
66
|
DeleteKey,
|
58
67
|
DownloadKey,
|
@@ -101,7 +110,10 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
101
110
|
CSS_PATH = "styles.tcss"
|
102
111
|
BINDINGS: ClassVar[list[BindingType]] = [
|
103
112
|
Binding("ctrl+r", "registrations", "Registrations list"),
|
104
|
-
Binding("ctrl+
|
113
|
+
Binding("ctrl+l", "storages", "Storages list"),
|
114
|
+
Binding("ctrl+f", "find", show=False),
|
115
|
+
Binding("ctrl+s", "settings", "Settings"),
|
116
|
+
Binding("ctrl+a", "about", "About"),
|
105
117
|
Binding(
|
106
118
|
KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
|
107
119
|
),
|
@@ -113,15 +125,24 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
113
125
|
|
114
126
|
def __init__(
|
115
127
|
self,
|
128
|
+
settings_service: SettingsService = Provide[DiContainer.settings_service],
|
116
129
|
credentials_service: CredentialsService = Provide[
|
117
130
|
DiContainer.credentials_service
|
118
131
|
],
|
132
|
+
package_meta_service: BasePackageMetaService = Provide[
|
133
|
+
DiContainer.package_meta_service
|
134
|
+
],
|
119
135
|
*args,
|
120
136
|
**kwargs,
|
121
137
|
):
|
122
138
|
super().__init__(*args, **kwargs)
|
139
|
+
self.settings_service = settings_service
|
123
140
|
self.credentials_service = credentials_service
|
124
|
-
self.
|
141
|
+
self.package_meta_service = package_meta_service
|
142
|
+
self.settings = self.settings_service.load_settings()
|
143
|
+
self.storage_list_sidebar = StorageListSidebar(
|
144
|
+
self.settings.group_by_access_credentials, id="storage_list_sidebar"
|
145
|
+
)
|
125
146
|
self.storage_content = StorageContentContainer(id="storage_content_container")
|
126
147
|
self.load_percentage = 0
|
127
148
|
self.active_resizing_rule: ResizingRule | None = None
|
@@ -142,16 +163,58 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
142
163
|
def _handle_exception(self, error: Exception) -> None:
|
143
164
|
self.push_screen(CriticalErrorScreen(str(error), traceback.format_exc()))
|
144
165
|
|
166
|
+
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
|
167
|
+
yield SystemCommand(
|
168
|
+
"Quit the application",
|
169
|
+
"Quit the application as soon as possible",
|
170
|
+
self.action_quit,
|
171
|
+
)
|
172
|
+
|
173
|
+
if screen.query("HelpPanel"):
|
174
|
+
yield SystemCommand(
|
175
|
+
"Hide keys and help panel",
|
176
|
+
"Hide the keys and widget help panel",
|
177
|
+
self.action_hide_help_panel,
|
178
|
+
)
|
179
|
+
else:
|
180
|
+
yield SystemCommand(
|
181
|
+
"Show keys and help panel",
|
182
|
+
"Show help for the focused widget and a summary of available keys",
|
183
|
+
self.action_show_help_panel,
|
184
|
+
)
|
185
|
+
yield SystemCommand(
|
186
|
+
"Save screenshot",
|
187
|
+
"Save an SVG 'screenshot' of the current screen",
|
188
|
+
self.deliver_screenshot,
|
189
|
+
)
|
190
|
+
yield SystemCommand("About", "About sourcerer", self.action_about)
|
191
|
+
yield SystemCommand("Settings", "Sourcerer settings", self.action_settings)
|
192
|
+
|
145
193
|
def on_mount(self):
|
146
194
|
"""
|
147
195
|
Initializes the application theme and storage list on mount.
|
148
196
|
"""
|
149
197
|
|
150
198
|
self.register_theme(github_dark_theme) # pyright: ignore [reportArgumentType]
|
151
|
-
|
152
|
-
|
199
|
+
if self.settings.theme in self._registered_themes:
|
200
|
+
self.theme = self.settings.theme
|
201
|
+
|
202
|
+
package_meta = self.package_meta_service.get_package_meta()
|
203
|
+
if package_meta.has_available_update:
|
204
|
+
self.notify(
|
205
|
+
f"Sourcerer {package_meta.version} "
|
206
|
+
f"is running while {package_meta.latest_version} is available",
|
207
|
+
severity="warning",
|
208
|
+
)
|
153
209
|
self.init_storages_list()
|
154
210
|
|
211
|
+
def action_find(self):
|
212
|
+
"""
|
213
|
+
Focus search input.
|
214
|
+
"""
|
215
|
+
with contextlib.suppress(NoMatches):
|
216
|
+
self.query_one(f"#{self.storage_content.search_input_id}").focus()
|
217
|
+
|
155
218
|
def action_focus_content(self):
|
156
219
|
"""
|
157
220
|
Focuses the storage content container.
|
@@ -174,10 +237,53 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
174
237
|
This method is typically used to allow users to add their
|
175
238
|
cloud storage credentials, which will then be reflected in the storage
|
176
239
|
"""
|
177
|
-
self.app.push_screen(
|
240
|
+
self.app.push_screen(
|
241
|
+
ProviderCredsListScreen(), callback=self.modal_screen_callback
|
242
|
+
)
|
243
|
+
|
244
|
+
def action_settings(self):
|
245
|
+
"""
|
246
|
+
Opens the settings screen.
|
247
|
+
This method is triggered by the key binding "ctrl+s" and allows the user
|
248
|
+
to modify application settings such as theme and grouping of storage items.
|
249
|
+
It retrieves the current settings from the settings service and pushes
|
250
|
+
the SettingsScreen to the application stack. A callback is set to handle
|
251
|
+
the settings changes when the screen is closed.
|
252
|
+
"""
|
253
|
+
settings = self.settings_service.load_settings()
|
254
|
+
self.app.push_screen(SettingsScreen(settings), callback=self.settings_callback)
|
178
255
|
|
179
256
|
def action_storages(self):
|
180
|
-
self.app.push_screen(StoragesListScreen(), callback=self.
|
257
|
+
self.app.push_screen(StoragesListScreen(), callback=self.modal_screen_callback)
|
258
|
+
|
259
|
+
def action_about(self):
|
260
|
+
self.push_screen(AboutScreen())
|
261
|
+
|
262
|
+
def settings_callback(self, settings: dict | None):
|
263
|
+
default_settings = self.settings_service.load_settings()
|
264
|
+
if settings is None:
|
265
|
+
self.app.theme = default_settings.theme
|
266
|
+
return
|
267
|
+
self.app.theme = settings[SettingsFields.theme]
|
268
|
+
if (theme := settings.get(SettingsFields.theme)) != default_settings.theme:
|
269
|
+
self.settings_service.set_setting(SettingsFields.theme, theme) # type: ignore
|
270
|
+
|
271
|
+
if (
|
272
|
+
group_by := settings.get(SettingsFields.group_by_access_credentials)
|
273
|
+
) != default_settings.group_by_access_credentials:
|
274
|
+
self.settings_service.set_setting(SettingsFields.group_by_access_credentials, group_by) # type: ignore
|
275
|
+
self.storage_list_sidebar.groupby_access_credentials = group_by # type: ignore
|
276
|
+
|
277
|
+
def modal_screen_callback(self, requires_storage_refresh: bool | None = True):
|
278
|
+
"""
|
279
|
+
Callback for modal screens to refresh the storage list if required.
|
280
|
+
|
281
|
+
This method is called when a modal screen is closed. If the
|
282
|
+
`requires_storage_refresh` flag is set to True, it refreshes the
|
283
|
+
storage list by calling the `refresh_storages` method.
|
284
|
+
"""
|
285
|
+
if requires_storage_refresh:
|
286
|
+
self.refresh_storages()
|
181
287
|
|
182
288
|
def refresh_storages(self, *args, **kwargs):
|
183
289
|
"""
|
@@ -187,6 +293,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
187
293
|
configurations.
|
188
294
|
"""
|
189
295
|
self.storage_list_sidebar.storages = {}
|
296
|
+
self.storage_list_sidebar.last_update_timestamp = time.time()
|
190
297
|
self.init_storages_list()
|
191
298
|
|
192
299
|
@work(thread=True)
|
@@ -384,6 +491,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
384
491
|
PreviewContentScreen(
|
385
492
|
storage_name=event.storage_name,
|
386
493
|
key=event.path,
|
494
|
+
file_size=event.size,
|
387
495
|
access_credentials_uuid=event.access_credentials_uuid,
|
388
496
|
)
|
389
497
|
)
|
@@ -562,7 +670,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
562
670
|
for storage in credentials.storages
|
563
671
|
if storage.name not in storage_names
|
564
672
|
]
|
565
|
-
self.storage_list_sidebar.storages[credentials.uuid] = (
|
673
|
+
self.storage_list_sidebar.storages[(credentials.uuid, credentials.name)] = (
|
566
674
|
storages + registered_storages
|
567
675
|
)
|
568
676
|
self.storage_list_sidebar.last_update_timestamp = time.time()
|
@@ -12,9 +12,18 @@ StorageContent {
|
|
12
12
|
align-horizontal: center;
|
13
13
|
|
14
14
|
& > Button {
|
15
|
+
color: $primary;
|
15
16
|
border: solid $primary;
|
16
|
-
|
17
|
-
|
17
|
+
padding-top: 0;
|
18
|
+
width: 15;
|
19
|
+
text-align: center;
|
20
|
+
|
21
|
+
&:hover {
|
22
|
+
color: $text;
|
23
|
+
}
|
24
|
+
&:focus {
|
25
|
+
color: $text;
|
26
|
+
}
|
18
27
|
}
|
19
28
|
}
|
20
29
|
|
@@ -25,8 +34,8 @@ StorageContent {
|
|
25
34
|
padding: 1 1 0 1;
|
26
35
|
}
|
27
36
|
|
28
|
-
|
37
|
+
ResizingRule {
|
29
38
|
color: $background-lighten-3;
|
30
39
|
padding: 0 0;
|
31
40
|
margin: 0 0;
|
32
|
-
}
|
41
|
+
}
|
@@ -12,6 +12,7 @@ from dataclasses import dataclass
|
|
12
12
|
from enum import Enum, auto
|
13
13
|
from typing import ClassVar, Self
|
14
14
|
|
15
|
+
import humanize
|
15
16
|
from textual import events, on
|
16
17
|
from textual.app import ComposeResult
|
17
18
|
from textual.binding import Binding, BindingType
|
@@ -308,6 +309,7 @@ class FileItem(StorageContentItem):
|
|
308
309
|
"""Message sent when a file preview is selected."""
|
309
310
|
|
310
311
|
name: str
|
312
|
+
size: int
|
311
313
|
|
312
314
|
@dataclass
|
313
315
|
class Unselect(Message):
|
@@ -333,7 +335,9 @@ class FileItem(StorageContentItem):
|
|
333
335
|
yield FileMetaLabel(
|
334
336
|
f"{FILE_ICON} {self.file.key}", classes="file_name", markup=False
|
335
337
|
)
|
336
|
-
yield FileMetaLabel(
|
338
|
+
yield FileMetaLabel(
|
339
|
+
f"{humanize.naturalsize(self.file.size)}", classes="file_size", markup=False
|
340
|
+
)
|
337
341
|
yield FileMetaLabel(
|
338
342
|
str(self.file.date_modified), classes="file_date", markup=False
|
339
343
|
)
|
@@ -360,7 +364,7 @@ class FileItem(StorageContentItem):
|
|
360
364
|
preview_button = self.query_one(Button)
|
361
365
|
|
362
366
|
if widget is preview_button:
|
363
|
-
self.post_message(self.Preview(self.file.key))
|
367
|
+
self.post_message(self.Preview(self.file.key, self.file.size))
|
364
368
|
return
|
365
369
|
|
366
370
|
checkbox = self.query_one(UnfocusableCheckbox)
|
@@ -561,6 +565,8 @@ class StorageContentContainer(Vertical):
|
|
561
565
|
}
|
562
566
|
"""
|
563
567
|
|
568
|
+
search_input_id: ClassVar[str] = "search_input"
|
569
|
+
|
564
570
|
def compose(self) -> ComposeResult:
|
565
571
|
if not self.storage:
|
566
572
|
return
|
@@ -584,7 +590,7 @@ class StorageContentContainer(Vertical):
|
|
584
590
|
with Horizontal():
|
585
591
|
yield Label("Search:")
|
586
592
|
yield Input(
|
587
|
-
id=
|
593
|
+
id=self.search_input_id,
|
588
594
|
placeholder="input path prefix here...",
|
589
595
|
value=self.search_prefix,
|
590
596
|
)
|
@@ -663,6 +669,7 @@ class StorageContentContainer(Vertical):
|
|
663
669
|
self.storage,
|
664
670
|
self.access_credentials_uuid,
|
665
671
|
os.path.join(self.path, event.name) if self.path else event.name,
|
672
|
+
event.size,
|
666
673
|
)
|
667
674
|
)
|
668
675
|
|
@@ -32,12 +32,14 @@ from sourcerer.presentation.screens.shared.widgets.button import Button
|
|
32
32
|
from sourcerer.presentation.screens.shared.widgets.spinner import Spinner
|
33
33
|
from sourcerer.presentation.settings import KeyBindings
|
34
34
|
|
35
|
+
"""Mapping of storage provider types to their display icons."""
|
35
36
|
STORAGE_ICONS = {
|
36
37
|
StorageProvider.S3: "🟠",
|
37
38
|
StorageProvider.GoogleCloudStorage: "🔵",
|
38
39
|
StorageProvider.AzureStorage: "⚪️",
|
39
40
|
}
|
40
|
-
|
41
|
+
|
42
|
+
StorageData = namedtuple("Storage", ["access_credentials_uuid", "storage"])
|
41
43
|
|
42
44
|
|
43
45
|
class StorageItem(Label):
|
@@ -140,7 +142,8 @@ class StorageListSidebar(Vertical):
|
|
140
142
|
"""
|
141
143
|
|
142
144
|
is_loading: reactive[bool] = reactive(False, recompose=True)
|
143
|
-
|
145
|
+
groupby_access_credentials: reactive[bool] = reactive(False, recompose=True)
|
146
|
+
storages: reactive[dict[tuple[str, str], list[Storage]]] = reactive({})
|
144
147
|
last_update_timestamp: reactive[float] = reactive( # ty: ignore[invalid-assignment]
|
145
148
|
0.0, recompose=True
|
146
149
|
)
|
@@ -151,12 +154,43 @@ class StorageListSidebar(Vertical):
|
|
151
154
|
margin-right: 0;
|
152
155
|
height: 100%;
|
153
156
|
margin-bottom: 1;
|
154
|
-
|
157
|
+
.rule-left {
|
155
158
|
width: 1;
|
159
|
+
color: $background-lighten-3;
|
160
|
+
}
|
161
|
+
|
162
|
+
.storage-credentials-container {
|
163
|
+
margin-top: 1;
|
164
|
+
|
165
|
+
& > :first-of-type {
|
166
|
+
margin-top: 0;
|
167
|
+
}
|
156
168
|
}
|
157
169
|
|
158
170
|
ScrollVerticalContainerWithNoBindings{
|
159
171
|
height: 95%;
|
172
|
+
|
173
|
+
& > Horizontal {
|
174
|
+
height: auto;
|
175
|
+
|
176
|
+
& > .storage-credentials-name {
|
177
|
+
color: $secondary;
|
178
|
+
padding: 0 1;
|
179
|
+
}
|
180
|
+
|
181
|
+
& > Rule {
|
182
|
+
color: $background-lighten-3;
|
183
|
+
}
|
184
|
+
|
185
|
+
& > Rule.storage-credentials-rule-left {
|
186
|
+
width: 1;
|
187
|
+
color: $secondary;
|
188
|
+
}
|
189
|
+
& > Rule.storage-credentials-rule-right {
|
190
|
+
color: $secondary;
|
191
|
+
}
|
192
|
+
|
193
|
+
}
|
160
194
|
}
|
161
195
|
|
162
196
|
Horizontal {
|
@@ -186,20 +220,18 @@ class StorageListSidebar(Vertical):
|
|
186
220
|
}
|
187
221
|
"""
|
188
222
|
|
189
|
-
def
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
yield GradientWidget(
|
194
|
-
" SOURCERER" if self.is_loading else "🧙SOURCERER",
|
195
|
-
id="left-middle",
|
196
|
-
name="header_click",
|
197
|
-
)
|
223
|
+
def __init__(self, groupby_access_credentials, *args, **kwargs):
|
224
|
+
"""Initialize the StorageListSidebar widget."""
|
225
|
+
super().__init__(*args, **kwargs)
|
226
|
+
self.groupby_access_credentials = groupby_access_credentials
|
198
227
|
|
199
|
-
|
228
|
+
def render_ungrouped_storages(self) -> ComposeResult:
|
200
229
|
storages = [
|
201
230
|
StorageData(access_credentials_uuid, storage)
|
202
|
-
for
|
231
|
+
for (
|
232
|
+
access_credentials_uuid,
|
233
|
+
access_credentials_name,
|
234
|
+
), storages in self.storages.items()
|
203
235
|
for storage in storages
|
204
236
|
]
|
205
237
|
storages = sorted(storages, key=lambda x: x.storage.storage)
|
@@ -207,20 +239,72 @@ class StorageListSidebar(Vertical):
|
|
207
239
|
for letter, storages_group in groupby(
|
208
240
|
storages, key=lambda x: x.storage.storage[0]
|
209
241
|
):
|
210
|
-
|
211
242
|
yield Horizontal(
|
212
|
-
Rule(
|
243
|
+
Rule(classes="rule-left"),
|
213
244
|
Label(letter.upper(), classes="storage-letter"),
|
214
245
|
Rule(),
|
246
|
+
classes="storage-letter-container",
|
215
247
|
)
|
216
|
-
|
217
248
|
for item in storages_group:
|
218
249
|
yield StorageItem(
|
219
|
-
renderable=f
|
250
|
+
renderable=f"{STORAGE_ICONS.get(item.storage.provider, '')} {item.storage.storage}",
|
220
251
|
storage_name=item.storage.storage,
|
221
252
|
access_credentials_uuid=item.access_credentials_uuid,
|
222
253
|
)
|
223
254
|
|
255
|
+
def render_grouped_by_access_credentials_storages(self) -> ComposeResult:
|
256
|
+
"""Render storages grouped by access credentials."""
|
257
|
+
with ScrollVerticalContainerWithNoBindings():
|
258
|
+
for (
|
259
|
+
access_credentials_uuid,
|
260
|
+
access_credentials_name,
|
261
|
+
), storages in self.storages.items():
|
262
|
+
storages = sorted(
|
263
|
+
[
|
264
|
+
StorageData(access_credentials_uuid, storage)
|
265
|
+
for storage in storages
|
266
|
+
],
|
267
|
+
key=lambda x: x.storage.storage,
|
268
|
+
)
|
269
|
+
yield Horizontal(
|
270
|
+
Rule(classes="storage-credentials-rule-left"),
|
271
|
+
Label(
|
272
|
+
access_credentials_name.upper(),
|
273
|
+
classes="storage-credentials-name",
|
274
|
+
),
|
275
|
+
Rule(classes="storage-credentials-rule-right"),
|
276
|
+
classes="storage-credentials-container",
|
277
|
+
)
|
278
|
+
for letter, storages_group in groupby(
|
279
|
+
storages, key=lambda x: x.storage.storage[0]
|
280
|
+
):
|
281
|
+
yield Horizontal(
|
282
|
+
Rule(classes="rule-left"),
|
283
|
+
Label(letter.upper(), classes="storage-letter"),
|
284
|
+
Rule(),
|
285
|
+
classes="storage-letter-container",
|
286
|
+
)
|
287
|
+
for item in storages_group:
|
288
|
+
yield StorageItem(
|
289
|
+
renderable=f"{STORAGE_ICONS.get(item.storage.provider, '')} {item.storage.storage}",
|
290
|
+
storage_name=item.storage.storage,
|
291
|
+
access_credentials_uuid=item.access_credentials_uuid,
|
292
|
+
)
|
293
|
+
|
294
|
+
def compose(self) -> ComposeResult:
|
295
|
+
with Horizontal(id="header"):
|
296
|
+
if self.is_loading:
|
297
|
+
yield Spinner()
|
298
|
+
yield GradientWidget(
|
299
|
+
" SOURCERER" if self.is_loading else "🧙SOURCERER",
|
300
|
+
id="left-middle",
|
301
|
+
name="header_click",
|
302
|
+
)
|
303
|
+
if self.groupby_access_credentials:
|
304
|
+
yield from self.render_grouped_by_access_credentials_storages()
|
305
|
+
else:
|
306
|
+
yield from self.render_ungrouped_storages()
|
307
|
+
|
224
308
|
def focus(self, scroll_visible: bool = True) -> Self:
|
225
309
|
try:
|
226
310
|
content = self.query_one(StorageItem)
|