Skip to content

Commit c41cbf0

Browse files
committed
Add quick connect and subscription refresh UX
1 parent 67086bf commit c41cbf0

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ All notable changes to **decoder sec.** are documented here.
99
- In-app log console
1010
- Signed TestFlight builds
1111

12+
## [0.1.0-beta.12] — 2026-08-12
13+
14+
### Added
15+
- Список профилей: быстрый **Connect** (кнопка в строке + swipe)
16+
- Pull-to-refresh и массовое обновление подписок из меню
17+
1218
## [0.1.0-beta.11] — 2026-08-12
1319

1420
### Fixed

‎DecoderSec/Configurations/ConfigurationsView.swift‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct ConfigurationsView: View {
1717
@State private var isDownloading = false
1818
@State private var importErrorMessage: String?
1919
@State private var showSubscribe = false
20+
@State private var isRefreshingAll = false
2021

2122
private var activeID: UUID? { store.activeIDByCoreType[store.selectedCore] }
2223

@@ -28,6 +29,14 @@ struct ConfigurationsView: View {
2829
} label: {
2930
row(for: config)
3031
}
32+
.swipeActions(edge: .leading, allowsFullSwipe: false) {
33+
Button {
34+
quickConnect(config)
35+
} label: {
36+
Label(String(localized: "Connect"), systemImage: "bolt.fill")
37+
}
38+
.tint(.mint)
39+
}
3140
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
3241
if config.sourceURL != nil {
3342
Button {
@@ -70,6 +79,10 @@ struct ConfigurationsView: View {
7079
.padding(20)
7180
}
7281
}
82+
83+
.refreshable {
84+
await refreshSubscriptions()
85+
}
7386
.navigationTitle("\(store.selectedCore.displayName) configurations")
7487
.navigationBarTitleDisplayMode(.inline)
7588
.toolbar {
@@ -93,6 +106,13 @@ struct ConfigurationsView: View {
93106
} label: {
94107
Label("Subscribe", systemImage: "link")
95108
}
109+
if store.configurationsForSelectedCore.contains(where: { $0.sourceURL != nil }) {
110+
Button {
111+
Task { await refreshSubscriptions() }
112+
} label: {
113+
Label(String(localized: "Refresh subscriptions"), systemImage: "arrow.clockwise")
114+
}
115+
}
96116
} label: {
97117
Image(systemName: "plus")
98118
}
@@ -149,6 +169,15 @@ struct ConfigurationsView: View {
149169
.lineLimit(1)
150170
}
151171
Spacer()
172+
Button {
173+
quickConnect(config)
174+
} label: {
175+
Image(systemName: "bolt.circle.fill")
176+
.font(.title3)
177+
}
178+
.buttonStyle(.borderless)
179+
.foregroundStyle(activeID == config.id && tunnel.status.isActive ? Color.green : Color.accentColor)
180+
.accessibilityLabel(String(localized: "Connect"))
152181
}
153182
.contentShape(Rectangle())
154183
}
@@ -248,6 +277,46 @@ struct ConfigurationsView: View {
248277
}
249278
}
250279

280+
281+
private func quickConnect(_ config: Configuration) {
282+
Task { @MainActor in
283+
if tunnel.status.isActive {
284+
await tunnel.setEnabled(false, configuration: store.active)
285+
}
286+
store.setActive(config)
287+
await tunnel.setEnabled(true, configuration: config)
288+
if let err = tunnel.lastError { importErrorMessage = err }
289+
}
290+
}
291+
292+
private func refreshSubscriptions() async {
293+
guard !isRefreshingAll else { return }
294+
let targets = store.configurationsForSelectedCore.filter { $0.sourceURL != nil }
295+
guard !targets.isEmpty else { return }
296+
297+
isRefreshingAll = true
298+
isDownloading = true
299+
defer {
300+
isRefreshingAll = false
301+
isDownloading = false
302+
}
303+
304+
var failCount = 0
305+
for config in targets {
306+
do {
307+
guard let raw = config.sourceURL, let url = URL(string: raw) else { continue }
308+
let result = try await SubscriptionImporter.fetch(from: url)
309+
store.update(config, content: result.content)
310+
} catch {
311+
failCount += 1
312+
}
313+
}
314+
315+
if failCount > 0 {
316+
importErrorMessage = String(localized: "Some subscriptions failed to refresh (\(failCount)).")
317+
}
318+
}
319+
251320
private func derivedName(from url: URL) -> String {
252321
let stripped = url.deletingPathExtension().lastPathComponent
253322
.trimmingCharacters(in: .whitespacesAndNewlines)

‎DecoderSec/Localizable.xcstrings‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2261,6 +2261,54 @@
22612261
}
22622262
}
22632263
}
2264+
},
2265+
"Refresh subscriptions": {
2266+
"localizations": {
2267+
"en": {
2268+
"stringUnit": {
2269+
"state": "translated",
2270+
"value": "Refresh subscriptions"
2271+
}
2272+
},
2273+
"ru": {
2274+
"stringUnit": {
2275+
"state": "translated",
2276+
"value": "Обновить подписки"
2277+
}
2278+
}
2279+
}
2280+
},
2281+
"Some subscriptions failed to refresh (%lld).": {
2282+
"localizations": {
2283+
"en": {
2284+
"stringUnit": {
2285+
"state": "translated",
2286+
"value": "Some subscriptions failed to refresh (%lld)."
2287+
}
2288+
},
2289+
"ru": {
2290+
"stringUnit": {
2291+
"state": "translated",
2292+
"value": "Некоторые подписки не удалось обновить (%lld)."
2293+
}
2294+
}
2295+
}
2296+
},
2297+
"Some subscriptions failed to refresh (%@).": {
2298+
"localizations": {
2299+
"en": {
2300+
"stringUnit": {
2301+
"state": "translated",
2302+
"value": "Some subscriptions failed to refresh (%@)."
2303+
}
2304+
},
2305+
"ru": {
2306+
"stringUnit": {
2307+
"state": "translated",
2308+
"value": "Некоторые подписки не удалось обновить (%@)."
2309+
}
2310+
}
2311+
}
22642312
}
22652313
},
22662314
"version": "1.1"

0 commit comments

Comments
 (0)