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.
Files changed (42) hide show
  1. {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/METADATA +9 -8
  2. {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/RECORD +42 -23
  3. sourcerer/__init__.py +3 -1
  4. sourcerer/domain/package_meta/__init__.py +0 -0
  5. sourcerer/domain/package_meta/entities.py +9 -0
  6. sourcerer/domain/package_meta/services.py +9 -0
  7. sourcerer/domain/settings/__init__.py +0 -0
  8. sourcerer/domain/settings/entities.py +11 -0
  9. sourcerer/domain/settings/repositories.py +20 -0
  10. sourcerer/domain/settings/services.py +19 -0
  11. sourcerer/infrastructure/db/models.py +23 -0
  12. sourcerer/infrastructure/package_meta/__init__.py +0 -0
  13. sourcerer/infrastructure/package_meta/services.py +26 -0
  14. sourcerer/infrastructure/settings/__init__.py +0 -0
  15. sourcerer/infrastructure/settings/repositories.py +59 -0
  16. sourcerer/infrastructure/settings/services.py +16 -0
  17. sourcerer/infrastructure/storage_provider/services/gcp.py +0 -1
  18. sourcerer/infrastructure/utils.py +1 -0
  19. sourcerer/presentation/di_container.py +13 -0
  20. sourcerer/presentation/screens/about/__init__.py +0 -0
  21. sourcerer/presentation/screens/about/main.py +60 -0
  22. sourcerer/presentation/screens/about/styles.tcss +32 -0
  23. sourcerer/presentation/screens/file_system_finder/__init__.py +0 -0
  24. sourcerer/presentation/screens/main/main.py +89 -6
  25. sourcerer/presentation/screens/main/styles.tcss +13 -4
  26. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +102 -18
  27. sourcerer/presentation/screens/provider_creds_list/main.py +14 -4
  28. sourcerer/presentation/screens/provider_creds_list/styles.tcss +9 -0
  29. sourcerer/presentation/screens/settings/__init__.py +0 -0
  30. sourcerer/presentation/screens/settings/main.py +70 -0
  31. sourcerer/presentation/screens/settings/styles.tcss +44 -0
  32. sourcerer/presentation/screens/shared/widgets/button.py +11 -0
  33. sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -3
  34. sourcerer/presentation/screens/storage_action_progress/main.py +1 -2
  35. sourcerer/presentation/screens/storages_list/main.py +15 -4
  36. sourcerer/presentation/screens/storages_list/styles.tcss +7 -0
  37. sourcerer/presentation/settings.py +1 -0
  38. sourcerer/presentation/utils.py +1 -0
  39. sourcerer/utils.py +19 -1
  40. {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/WHEEL +0 -0
  41. {data_sourcerer-0.4.0.dist-info → data_sourcerer-0.5.0.dist-info}/entry_points.txt +0 -0
  42. {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+s", "storages", "Storages list"),
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.storage_list_sidebar = StorageListSidebar(id="storage_list_sidebar")
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
- self.theme = "github-dark"
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
- width: 10;
17
- content-align-horizontal: center;
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
- Rule {
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
- """Mapping of storage provider types to their display icons."""
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
- storages: reactive[dict[str, list[Storage]]] = reactive({})
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
- #rule-left {
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 compose(self) -> ComposeResult:
190
- with Horizontal(id="header"):
191
- if self.is_loading:
192
- yield Spinner()
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
- StorageData = namedtuple("Storage", ["access_credentials_uuid", "storage"])
228
+ def render_ungrouped_storages(self) -> ComposeResult:
200
229
  storages = [
201
230
  StorageData(access_credentials_uuid, storage)
202
- for access_credentials_uuid, storages in self.storages.items()
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(id="rule-left"),
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'{STORAGE_ICONS.get(item.storage.provider, "")} {item.storage.storage}',
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.app.push_screen(
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.app.push_screen(
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
+ )