data-sourcerer 0.4.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.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/METADATA +9 -8
- {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/RECORD +42 -23
- 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/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/gcp.py +0 -1
- 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/main/main.py +89 -6
- sourcerer/presentation/screens/main/styles.tcss +13 -4
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +102 -18
- sourcerer/presentation/screens/provider_creds_list/main.py +14 -4
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +9 -0
- 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/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 +15 -4
- sourcerer/presentation/screens/storages_list/styles.tcss +7 -0
- sourcerer/presentation/settings.py +1 -0
- sourcerer/presentation/utils.py +1 -0
- sourcerer/utils.py +19 -1
- {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,26 +1,32 @@
|
|
1
1
|
import contextlib
|
2
2
|
import time
|
3
3
|
import traceback
|
4
|
+
from collections.abc import Iterable
|
4
5
|
from concurrent.futures import ThreadPoolExecutor
|
5
6
|
from pathlib import Path
|
6
7
|
from typing import ClassVar
|
7
8
|
|
8
9
|
from dependency_injector.wiring import Provide
|
9
10
|
from textual import on, work
|
10
|
-
from textual.app import App, ComposeResult
|
11
|
+
from textual.app import App, ComposeResult, SystemCommand
|
11
12
|
from textual.binding import Binding, BindingType
|
12
13
|
from textual.containers import Horizontal
|
13
14
|
from textual.css.query import NoMatches
|
14
15
|
from textual.reactive import reactive
|
16
|
+
from textual.screen import Screen
|
15
17
|
from textual.widgets import Footer
|
16
18
|
|
19
|
+
from sourcerer.domain.package_meta.services import BasePackageMetaService
|
20
|
+
from sourcerer.domain.settings.entities import SettingsFields
|
17
21
|
from sourcerer.domain.storage_provider.entities import Storage
|
18
22
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
23
|
+
from sourcerer.infrastructure.settings.services import SettingsService
|
19
24
|
from sourcerer.infrastructure.storage_provider.exceptions import (
|
20
25
|
ListStorageItemsError,
|
21
26
|
)
|
22
27
|
from sourcerer.infrastructure.utils import generate_uuid
|
23
28
|
from sourcerer.presentation.di_container import DiContainer
|
29
|
+
from sourcerer.presentation.screens.about.main import AboutScreen
|
24
30
|
from sourcerer.presentation.screens.critical_error.main import CriticalErrorScreen
|
25
31
|
from sourcerer.presentation.screens.file_system_finder.main import (
|
26
32
|
FileSystemNavigationModal,
|
@@ -55,6 +61,7 @@ from sourcerer.presentation.screens.preview_content.main import PreviewContentSc
|
|
55
61
|
from sourcerer.presentation.screens.provider_creds_list.main import (
|
56
62
|
ProviderCredsListScreen,
|
57
63
|
)
|
64
|
+
from sourcerer.presentation.screens.settings.main import SettingsScreen
|
58
65
|
from sourcerer.presentation.screens.storage_action_progress.main import (
|
59
66
|
DeleteKey,
|
60
67
|
DownloadKey,
|
@@ -103,8 +110,10 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
103
110
|
CSS_PATH = "styles.tcss"
|
104
111
|
BINDINGS: ClassVar[list[BindingType]] = [
|
105
112
|
Binding("ctrl+r", "registrations", "Registrations list"),
|
106
|
-
Binding("ctrl+
|
113
|
+
Binding("ctrl+l", "storages", "Storages list"),
|
107
114
|
Binding("ctrl+f", "find", show=False),
|
115
|
+
Binding("ctrl+s", "settings", "Settings"),
|
116
|
+
Binding("ctrl+a", "about", "About"),
|
108
117
|
Binding(
|
109
118
|
KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
|
110
119
|
),
|
@@ -116,15 +125,24 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
116
125
|
|
117
126
|
def __init__(
|
118
127
|
self,
|
128
|
+
settings_service: SettingsService = Provide[DiContainer.settings_service],
|
119
129
|
credentials_service: CredentialsService = Provide[
|
120
130
|
DiContainer.credentials_service
|
121
131
|
],
|
132
|
+
package_meta_service: BasePackageMetaService = Provide[
|
133
|
+
DiContainer.package_meta_service
|
134
|
+
],
|
122
135
|
*args,
|
123
136
|
**kwargs,
|
124
137
|
):
|
125
138
|
super().__init__(*args, **kwargs)
|
139
|
+
self.settings_service = settings_service
|
126
140
|
self.credentials_service = credentials_service
|
127
|
-
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
|
+
)
|
128
146
|
self.storage_content = StorageContentContainer(id="storage_content_container")
|
129
147
|
self.load_percentage = 0
|
130
148
|
self.active_resizing_rule: ResizingRule | None = None
|
@@ -145,14 +163,49 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
145
163
|
def _handle_exception(self, error: Exception) -> None:
|
146
164
|
self.push_screen(CriticalErrorScreen(str(error), traceback.format_exc()))
|
147
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
|
+
|
148
193
|
def on_mount(self):
|
149
194
|
"""
|
150
195
|
Initializes the application theme and storage list on mount.
|
151
196
|
"""
|
152
197
|
|
153
198
|
self.register_theme(github_dark_theme) # pyright: ignore [reportArgumentType]
|
154
|
-
|
155
|
-
|
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
|
+
)
|
156
209
|
self.init_storages_list()
|
157
210
|
|
158
211
|
def action_find(self):
|
@@ -188,9 +241,39 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
188
241
|
ProviderCredsListScreen(), callback=self.modal_screen_callback
|
189
242
|
)
|
190
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)
|
255
|
+
|
191
256
|
def action_storages(self):
|
192
257
|
self.app.push_screen(StoragesListScreen(), callback=self.modal_screen_callback)
|
193
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
|
+
|
194
277
|
def modal_screen_callback(self, requires_storage_refresh: bool | None = True):
|
195
278
|
"""
|
196
279
|
Callback for modal screens to refresh the storage list if required.
|
@@ -587,7 +670,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
|
|
587
670
|
for storage in credentials.storages
|
588
671
|
if storage.name not in storage_names
|
589
672
|
]
|
590
|
-
self.storage_list_sidebar.storages[credentials.uuid] = (
|
673
|
+
self.storage_list_sidebar.storages[(credentials.uuid, credentials.name)] = (
|
591
674
|
storages + registered_storages
|
592
675
|
)
|
593
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
|
+
}
|
@@ -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)
|
@@ -1,9 +1,11 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
from enum import Enum
|
3
|
+
from typing import ClassVar
|
3
4
|
|
4
5
|
from dependency_injector.wiring import Provide
|
5
6
|
from textual import on
|
6
7
|
from textual.app import ComposeResult
|
8
|
+
from textual.binding import Binding, BindingType
|
7
9
|
from textual.containers import Container, Horizontal, VerticalScroll
|
8
10
|
from textual.message import Message
|
9
11
|
from textual.reactive import reactive
|
@@ -112,6 +114,11 @@ class ProviderCredsListScreen(RefreshTriggerableModalScreen):
|
|
112
114
|
PROVIDERS_NAME = "providers"
|
113
115
|
AUTH_METHODS_NAME = "auth_methods"
|
114
116
|
|
117
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
118
|
+
*RefreshTriggerableModalScreen.BINDINGS,
|
119
|
+
Binding("ctrl+n", "add_credentials", "Add new credentials"),
|
120
|
+
]
|
121
|
+
|
115
122
|
credentials_list = reactive([], recompose=True)
|
116
123
|
|
117
124
|
def __init__(
|
@@ -201,10 +208,7 @@ class ProviderCredsListScreen(RefreshTriggerableModalScreen):
|
|
201
208
|
if event.action == ControlsEnum.CANCEL.name:
|
202
209
|
self.action_cancel_screen()
|
203
210
|
if event.action == "add_registration":
|
204
|
-
self.
|
205
|
-
ProviderCredsRegistrationScreen(),
|
206
|
-
callback=self.create_provider_creds_registration, # type: ignore
|
207
|
-
)
|
211
|
+
self.action_add_credentials()
|
208
212
|
|
209
213
|
@on(ProviderCredentialsRow.ChangeActiveStatus)
|
210
214
|
def on_change_active_status(self, event: ProviderCredentialsRow.ChangeActiveStatus):
|
@@ -231,3 +235,9 @@ class ProviderCredsListScreen(RefreshTriggerableModalScreen):
|
|
231
235
|
_ (ReloadCredentialsRequest): The reload credentials request event.
|
232
236
|
"""
|
233
237
|
self.refresh_credentials_list()
|
238
|
+
|
239
|
+
def action_add_credentials(self):
|
240
|
+
self.app.push_screen(
|
241
|
+
ProviderCredsRegistrationScreen(),
|
242
|
+
callback=self.create_provider_creds_registration, # type: ignore
|
243
|
+
)
|
@@ -23,6 +23,14 @@ ProviderCredsListScreen {
|
|
23
23
|
|
24
24
|
.add_registration_button {
|
25
25
|
color: $success-darken-2;
|
26
|
+
|
27
|
+
&:hover {
|
28
|
+
color: $primary;
|
29
|
+
}
|
30
|
+
&:focus {
|
31
|
+
color: $primary;
|
32
|
+
}
|
33
|
+
|
26
34
|
}
|
27
35
|
|
28
36
|
margin-bottom: 1;
|
@@ -30,6 +38,7 @@ ProviderCredsListScreen {
|
|
30
38
|
|
31
39
|
ProviderCredentialsRow.active {
|
32
40
|
background: $primary-lighten-2;
|
41
|
+
color: $background-darken-3;
|
33
42
|
}
|
34
43
|
|
35
44
|
ProviderCredentialsRow, Horizontal {
|
File without changes
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from textual import on
|
2
|
+
from textual.app import ComposeResult
|
3
|
+
from textual.containers import Container, Horizontal
|
4
|
+
from textual.widgets import Checkbox, Rule, Select, Static
|
5
|
+
|
6
|
+
from sourcerer.domain.settings.entities import Settings, SettingsFields
|
7
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
8
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
9
|
+
|
10
|
+
|
11
|
+
class SettingsScreen(ExitBoundModalScreen):
|
12
|
+
"""Screen with a parameter."""
|
13
|
+
|
14
|
+
CSS_PATH = "styles.tcss"
|
15
|
+
|
16
|
+
def __init__(self, settings: Settings) -> None:
|
17
|
+
super().__init__()
|
18
|
+
self.settings = settings
|
19
|
+
|
20
|
+
def compose(self) -> ComposeResult:
|
21
|
+
with Container():
|
22
|
+
with Horizontal():
|
23
|
+
yield Static("Theme:")
|
24
|
+
yield Select(
|
25
|
+
((theme, theme) for theme in self.app._registered_themes),
|
26
|
+
id="theme",
|
27
|
+
value=self.settings.theme,
|
28
|
+
allow_blank=False,
|
29
|
+
)
|
30
|
+
|
31
|
+
yield Rule()
|
32
|
+
with Horizontal():
|
33
|
+
yield Checkbox(
|
34
|
+
"Group storage by access credentials",
|
35
|
+
value=self.settings.group_by_access_credentials,
|
36
|
+
)
|
37
|
+
|
38
|
+
yield Rule()
|
39
|
+
with Horizontal(id="controls"):
|
40
|
+
yield Button("Save", name="save")
|
41
|
+
yield Button("Close", name="close")
|
42
|
+
|
43
|
+
@on(Button.Click)
|
44
|
+
def on_button_clicked(self, event: Button.Click) -> None:
|
45
|
+
"""Handle button clicked events."""
|
46
|
+
if event.action == "close":
|
47
|
+
self.action_cancel_screen()
|
48
|
+
elif event.action == "save":
|
49
|
+
self.dismiss(
|
50
|
+
{
|
51
|
+
SettingsFields.theme: self.query_one("Select#theme", Select).value,
|
52
|
+
SettingsFields.group_by_access_credentials: self.query_one(
|
53
|
+
Checkbox
|
54
|
+
).value,
|
55
|
+
}
|
56
|
+
)
|
57
|
+
|
58
|
+
def action_cancel_screen(self):
|
59
|
+
self.dismiss(
|
60
|
+
{
|
61
|
+
SettingsFields.theme: self.settings.theme,
|
62
|
+
SettingsFields.group_by_access_credentials: self.settings.group_by_access_credentials,
|
63
|
+
}
|
64
|
+
)
|
65
|
+
|
66
|
+
@on(Select.Changed)
|
67
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
68
|
+
"""Handle select changed events."""
|
69
|
+
if event.select.id == "theme":
|
70
|
+
self.app.theme = event.value # type: ignore[assignment]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
SettingsScreen {
|
2
|
+
align: center middle;
|
3
|
+
content-align: center top;
|
4
|
+
|
5
|
+
& > Container {
|
6
|
+
padding: 1 2 0 2;
|
7
|
+
margin: 0 0;
|
8
|
+
width: 70%;
|
9
|
+
height: auto;
|
10
|
+
border: solid $border;
|
11
|
+
|
12
|
+
& > Static {
|
13
|
+
text-align: center;
|
14
|
+
text-wrap: wrap;
|
15
|
+
}
|
16
|
+
|
17
|
+
& > Horizontal {
|
18
|
+
align: center bottom;
|
19
|
+
height: auto;
|
20
|
+
|
21
|
+
& > Static {
|
22
|
+
width: 10%;
|
23
|
+
padding-top: 1;
|
24
|
+
}
|
25
|
+
|
26
|
+
& > Checkbox {
|
27
|
+
width: 100%;
|
28
|
+
border: none;
|
29
|
+
background: transparent;
|
30
|
+
|
31
|
+
&:focus {
|
32
|
+
background: transparent;
|
33
|
+
|
34
|
+
& > .toggle--label {
|
35
|
+
color: $text-secondary;
|
36
|
+
background: transparent;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
}
|
@@ -28,6 +28,7 @@ class Button(Label):
|
|
28
28
|
}
|
29
29
|
}
|
30
30
|
"""
|
31
|
+
can_focus = True
|
31
32
|
|
32
33
|
@dataclass
|
33
34
|
class Click(Message):
|
@@ -52,3 +53,13 @@ class Button(Label):
|
|
52
53
|
_: An instance of events.Click representing the click event.
|
53
54
|
"""
|
54
55
|
self.post_message(self.Click(self.name)) # type: ignore
|
56
|
+
|
57
|
+
def on_key(self, event: events.Key) -> None:
|
58
|
+
"""
|
59
|
+
Handle key events to trigger click action when the button is focused and activated.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
event (events.Key): The key event to handle.
|
63
|
+
"""
|
64
|
+
if event.key == "enter":
|
65
|
+
self.post_message(self.Click(self.name)) # type: ignore
|
@@ -77,8 +77,6 @@ class LabeledInput(Container):
|
|
77
77
|
"""
|
78
78
|
input_area = self.query_one(".form_input")
|
79
79
|
text = (
|
80
|
-
input_area.document.text
|
81
|
-
if isinstance(input_area, TextArea)
|
82
|
-
else input_area.value # type: ignore
|
80
|
+
input_area.document.text if isinstance(input_area, TextArea) else input_area.value # type: ignore
|
83
81
|
)
|
84
82
|
return self.Value(name=self.key, value=text)
|
@@ -369,7 +369,7 @@ class StorageActionProgressScreen(ModalScreen):
|
|
369
369
|
key (str): The key/path of the file to delete
|
370
370
|
uuid (str): Unique identifier for the file
|
371
371
|
main_progress_bar (ProgressBar): The main progress bar to update
|
372
|
-
"""
|
372
|
+
"""
|
373
373
|
if not self.provider_service:
|
374
374
|
self.notify(f"Failed to delete {key}", severity="error")
|
375
375
|
main_progress_bar.advance(1)
|
@@ -418,7 +418,6 @@ class StorageActionProgressScreen(ModalScreen):
|
|
418
418
|
finally:
|
419
419
|
self.files_has_been_processed = True
|
420
420
|
elif source_path.is_dir():
|
421
|
-
|
422
421
|
files_n = len([i for i in source_path.rglob("*") if i.is_file()])
|
423
422
|
progress_bar = self.query_one(f"#progress_bar_{key.uuid}", ProgressBar)
|
424
423
|
progress_bar.total = files_n
|
@@ -1,10 +1,12 @@
|
|
1
1
|
import datetime
|
2
2
|
import uuid
|
3
3
|
from enum import Enum
|
4
|
+
from typing import ClassVar
|
4
5
|
|
5
6
|
from dependency_injector.wiring import Provide
|
6
7
|
from textual import on
|
7
8
|
from textual.app import ComposeResult
|
9
|
+
from textual.binding import Binding, BindingType
|
8
10
|
from textual.containers import Container, Horizontal, VerticalScroll
|
9
11
|
from textual.reactive import reactive
|
10
12
|
from textual.widgets import Label
|
@@ -82,6 +84,11 @@ class StoragesListScreen(RefreshTriggerableModalScreen):
|
|
82
84
|
MAIN_CONTAINER_ID = "StoragesListScreen"
|
83
85
|
SETTINGS_CONTAINER_ID = "settings"
|
84
86
|
|
87
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
88
|
+
*RefreshTriggerableModalScreen.BINDINGS,
|
89
|
+
Binding("ctrl+n", "add_storage", "Add new storage"),
|
90
|
+
]
|
91
|
+
|
85
92
|
storages_list = reactive([], recompose=True)
|
86
93
|
|
87
94
|
def __init__(
|
@@ -104,6 +111,7 @@ class StoragesListScreen(RefreshTriggerableModalScreen):
|
|
104
111
|
"+Add new storage",
|
105
112
|
name=ControlsEnum.ADD_STORAGE.name,
|
106
113
|
classes="add_storage_button",
|
114
|
+
id="add_storage_button",
|
107
115
|
),
|
108
116
|
id="right-top",
|
109
117
|
)
|
@@ -155,10 +163,7 @@ class StoragesListScreen(RefreshTriggerableModalScreen):
|
|
155
163
|
if event.action == ControlsEnum.CANCEL.name:
|
156
164
|
self.action_cancel_screen()
|
157
165
|
if event.action == ControlsEnum.ADD_STORAGE.name:
|
158
|
-
self.
|
159
|
-
StoragesRegistrationScreen(),
|
160
|
-
callback=self.create_storage_entry, # type: ignore
|
161
|
-
)
|
166
|
+
self.action_add_storage()
|
162
167
|
|
163
168
|
def create_storage_entry(self, storage: StorageEntry | None):
|
164
169
|
"""
|
@@ -182,3 +187,9 @@ class StoragesListScreen(RefreshTriggerableModalScreen):
|
|
182
187
|
)
|
183
188
|
)
|
184
189
|
self.refresh_storages_list()
|
190
|
+
|
191
|
+
def action_add_storage(self):
|
192
|
+
self.app.push_screen(
|
193
|
+
StoragesRegistrationScreen(),
|
194
|
+
callback=self.create_storage_entry, # type: ignore
|
195
|
+
)
|