s3ui 1.0.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.
@@ -0,0 +1,328 @@
1
+ """Settings dialog with tabs for credentials, transfers, costs, and general."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PyQt6.QtCore import pyqtSignal
9
+ from PyQt6.QtWidgets import (
10
+ QCheckBox,
11
+ QComboBox,
12
+ QDialog,
13
+ QDialogButtonBox,
14
+ QFileDialog,
15
+ QFormLayout,
16
+ QHBoxLayout,
17
+ QLabel,
18
+ QLineEdit,
19
+ QListWidget,
20
+ QMessageBox,
21
+ QPushButton,
22
+ QSpinBox,
23
+ QTabWidget,
24
+ QVBoxLayout,
25
+ QWidget,
26
+ )
27
+
28
+ from s3ui.core.credentials import CredentialStore, Profile, discover_aws_profiles
29
+ from s3ui.db.database import get_bool_pref, get_int_pref, get_pref, set_pref
30
+ from s3ui.ui.setup_wizard import AWS_REGIONS
31
+
32
+ if TYPE_CHECKING:
33
+ from s3ui.db.database import Database
34
+
35
+ logger = logging.getLogger("s3ui.settings_dialog")
36
+
37
+
38
+ class CredentialsTab(QWidget):
39
+ """Tab for managing AWS credential profiles."""
40
+
41
+ profile_changed = pyqtSignal()
42
+
43
+ def __init__(self, store: CredentialStore, parent=None) -> None:
44
+ super().__init__(parent)
45
+ self._store = store
46
+
47
+ layout = QVBoxLayout(self)
48
+
49
+ # Profile list
50
+ layout.addWidget(QLabel("Saved Profiles:"))
51
+ self._profile_list = QListWidget()
52
+ layout.addWidget(self._profile_list)
53
+
54
+ # Buttons
55
+ btn_row = QHBoxLayout()
56
+ self._add_btn = QPushButton("Add Profile...")
57
+ self._add_btn.clicked.connect(self._on_add)
58
+ btn_row.addWidget(self._add_btn)
59
+
60
+ self._edit_btn = QPushButton("Edit...")
61
+ self._edit_btn.clicked.connect(self._on_edit)
62
+ self._edit_btn.setEnabled(False)
63
+ btn_row.addWidget(self._edit_btn)
64
+
65
+ self._delete_btn = QPushButton("Delete")
66
+ self._delete_btn.clicked.connect(self._on_delete)
67
+ self._delete_btn.setEnabled(False)
68
+ btn_row.addWidget(self._delete_btn)
69
+
70
+ btn_row.addStretch()
71
+ layout.addLayout(btn_row)
72
+
73
+ self._profile_list.currentItemChanged.connect(self._on_selection_changed)
74
+ self._refresh_list()
75
+
76
+ def _refresh_list(self) -> None:
77
+ self._profile_list.clear()
78
+ # Show AWS CLI profiles first
79
+ aws_profiles = discover_aws_profiles()
80
+ for name in aws_profiles:
81
+ self._profile_list.addItem(f"{name} (AWS CLI)")
82
+ # Then custom profiles from keyring
83
+ for name in self._store.list_profiles():
84
+ self._profile_list.addItem(name)
85
+
86
+ def _on_selection_changed(self) -> None:
87
+ has_selection = self._profile_list.currentItem() is not None
88
+ self._edit_btn.setEnabled(has_selection)
89
+ self._delete_btn.setEnabled(has_selection)
90
+
91
+ def _on_add(self) -> None:
92
+ dialog = _ProfileEditDialog(self._store, parent=self)
93
+ if dialog.exec() == QDialog.DialogCode.Accepted:
94
+ self._refresh_list()
95
+ self.profile_changed.emit()
96
+
97
+ def _on_edit(self) -> None:
98
+ item = self._profile_list.currentItem()
99
+ if not item:
100
+ return
101
+ profile = self._store.get_profile(item.text())
102
+ if profile:
103
+ dialog = _ProfileEditDialog(self._store, profile=profile, parent=self)
104
+ if dialog.exec() == QDialog.DialogCode.Accepted:
105
+ self._refresh_list()
106
+ self.profile_changed.emit()
107
+
108
+ def _on_delete(self) -> None:
109
+ item = self._profile_list.currentItem()
110
+ if not item:
111
+ return
112
+ name = item.text()
113
+ reply = QMessageBox.question(
114
+ self,
115
+ "Delete Profile",
116
+ f"Remove profile '{name}'?\n\nCredentials will be removed from the system keychain.",
117
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
118
+ )
119
+ if reply == QMessageBox.StandardButton.Yes:
120
+ self._store.delete_profile(name)
121
+ self._refresh_list()
122
+ self.profile_changed.emit()
123
+
124
+
125
+ class _ProfileEditDialog(QDialog):
126
+ """Dialog for adding or editing a credential profile."""
127
+
128
+ def __init__(
129
+ self,
130
+ store: CredentialStore,
131
+ profile: Profile | None = None,
132
+ parent=None,
133
+ ) -> None:
134
+ super().__init__(parent)
135
+ self._store = store
136
+ self._editing = profile is not None
137
+
138
+ self.setWindowTitle("Edit Profile" if self._editing else "Add Profile")
139
+ self.setMinimumWidth(400)
140
+
141
+ layout = QFormLayout(self)
142
+
143
+ self._name_edit = QLineEdit()
144
+ if profile:
145
+ self._name_edit.setText(profile.name)
146
+ self._name_edit.setReadOnly(True)
147
+ layout.addRow("Profile Name:", self._name_edit)
148
+
149
+ self._access_key_edit = QLineEdit()
150
+ if profile:
151
+ self._access_key_edit.setText(profile.access_key_id)
152
+ layout.addRow("Access Key ID:", self._access_key_edit)
153
+
154
+ self._secret_key_edit = QLineEdit()
155
+ self._secret_key_edit.setEchoMode(QLineEdit.EchoMode.Password)
156
+ if profile:
157
+ self._secret_key_edit.setText(profile.secret_access_key)
158
+ layout.addRow("Secret Access Key:", self._secret_key_edit)
159
+
160
+ self._region_combo = QComboBox()
161
+ for display_name, region_code in AWS_REGIONS:
162
+ self._region_combo.addItem(f"{display_name} ({region_code})", region_code)
163
+ if profile:
164
+ for i in range(self._region_combo.count()):
165
+ if self._region_combo.itemData(i) == profile.region:
166
+ self._region_combo.setCurrentIndex(i)
167
+ break
168
+ layout.addRow("Region:", self._region_combo)
169
+
170
+ self._endpoint_edit = QLineEdit()
171
+ self._endpoint_edit.setPlaceholderText("e.g., http://localhost:9000")
172
+ if profile:
173
+ self._endpoint_edit.setText(profile.endpoint_url)
174
+ layout.addRow("Endpoint URL (optional):", self._endpoint_edit)
175
+
176
+ buttons = QDialogButtonBox(
177
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
178
+ )
179
+ buttons.accepted.connect(self._on_accept)
180
+ buttons.rejected.connect(self.reject)
181
+ layout.addRow(buttons)
182
+
183
+ def _on_accept(self) -> None:
184
+ name = self._name_edit.text().strip()
185
+ access_key = self._access_key_edit.text().strip()
186
+ secret_key = self._secret_key_edit.text().strip()
187
+ region = self._region_combo.currentData()
188
+ endpoint_url = self._endpoint_edit.text().strip()
189
+
190
+ if not name or not access_key or not secret_key:
191
+ QMessageBox.warning(self, "Missing Fields", "Please fill in all fields.")
192
+ return
193
+
194
+ profile = Profile(
195
+ name=name,
196
+ access_key_id=access_key,
197
+ secret_access_key=secret_key,
198
+ region=region,
199
+ endpoint_url=endpoint_url,
200
+ )
201
+ self._store.save_profile(profile)
202
+ self.accept()
203
+
204
+
205
+ class TransfersTab(QWidget):
206
+ """Tab for transfer settings."""
207
+
208
+ def __init__(self, db: Database | None = None, parent=None) -> None:
209
+ super().__init__(parent)
210
+ self._db = db
211
+
212
+ layout = QFormLayout(self)
213
+
214
+ self._max_concurrent = QSpinBox()
215
+ self._max_concurrent.setRange(1, 16)
216
+ self._max_concurrent.setValue(4)
217
+ layout.addRow("Max concurrent transfers:", self._max_concurrent)
218
+
219
+ self._retention_combo = QComboBox()
220
+ self._retention_combo.addItems(
221
+ [
222
+ "Clear after session",
223
+ "Keep for 24 hours",
224
+ "Keep forever",
225
+ ]
226
+ )
227
+ layout.addRow("Completed transfer retention:", self._retention_combo)
228
+
229
+ if db:
230
+ val = get_int_pref(db, "max_concurrent_transfers", 4)
231
+ self._max_concurrent.setValue(val)
232
+ ret = get_pref(db, "transfer_retention", "Clear after session")
233
+ idx = self._retention_combo.findText(ret)
234
+ if idx >= 0:
235
+ self._retention_combo.setCurrentIndex(idx)
236
+
237
+ def apply_settings(self) -> None:
238
+ if self._db:
239
+ set_pref(self._db, "max_concurrent_transfers", str(self._max_concurrent.value()))
240
+ set_pref(self._db, "transfer_retention", self._retention_combo.currentText())
241
+
242
+
243
+ class GeneralTab(QWidget):
244
+ """Tab for general settings."""
245
+
246
+ def __init__(self, db: Database | None = None, parent=None) -> None:
247
+ super().__init__(parent)
248
+ self._db = db
249
+
250
+ layout = QFormLayout(self)
251
+
252
+ # Default local directory
253
+ dir_row = QHBoxLayout()
254
+ self._dir_edit = QLineEdit()
255
+ dir_row.addWidget(self._dir_edit)
256
+ browse_btn = QPushButton("Browse...")
257
+ browse_btn.clicked.connect(self._browse_directory)
258
+ dir_row.addWidget(browse_btn)
259
+ layout.addRow("Default local directory:", dir_row)
260
+
261
+ # Show hidden files
262
+ self._show_hidden = QCheckBox("Show hidden files by default")
263
+ layout.addRow(self._show_hidden)
264
+
265
+ if db:
266
+ from pathlib import Path
267
+
268
+ self._dir_edit.setText(get_pref(db, "default_local_dir", str(Path.home())))
269
+ self._show_hidden.setChecked(get_bool_pref(db, "show_hidden_files", False))
270
+
271
+ def _browse_directory(self) -> None:
272
+ path = QFileDialog.getExistingDirectory(
273
+ self, "Select Default Directory", self._dir_edit.text()
274
+ )
275
+ if path:
276
+ self._dir_edit.setText(path)
277
+
278
+ def apply_settings(self) -> None:
279
+ if self._db:
280
+ set_pref(self._db, "default_local_dir", self._dir_edit.text())
281
+ set_pref(self._db, "show_hidden_files", str(self._show_hidden.isChecked()))
282
+
283
+
284
+ class SettingsDialog(QDialog):
285
+ """Application settings dialog."""
286
+
287
+ settings_changed = pyqtSignal()
288
+
289
+ def __init__(
290
+ self,
291
+ store: CredentialStore | None = None,
292
+ db: Database | None = None,
293
+ parent=None,
294
+ ) -> None:
295
+ super().__init__(parent)
296
+ self._store = store or CredentialStore()
297
+ self._db = db
298
+ self.setWindowTitle("Settings")
299
+ self.setMinimumSize(500, 400)
300
+
301
+ layout = QVBoxLayout(self)
302
+
303
+ self._tabs = QTabWidget()
304
+
305
+ self._cred_tab = CredentialsTab(self._store)
306
+ self._tabs.addTab(self._cred_tab, "Credentials")
307
+
308
+ self._transfers_tab = TransfersTab(db)
309
+ self._tabs.addTab(self._transfers_tab, "Transfers")
310
+
311
+ self._general_tab = GeneralTab(db)
312
+ self._tabs.addTab(self._general_tab, "General")
313
+
314
+ layout.addWidget(self._tabs)
315
+
316
+ # Buttons
317
+ buttons = QDialogButtonBox(
318
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
319
+ )
320
+ buttons.accepted.connect(self._on_accept)
321
+ buttons.rejected.connect(self.reject)
322
+ layout.addWidget(buttons)
323
+
324
+ def _on_accept(self) -> None:
325
+ self._transfers_tab.apply_settings()
326
+ self._general_tab.apply_settings()
327
+ self.settings_changed.emit()
328
+ self.accept()