@@ -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 )
0 commit comments