@@ -163,34 +163,18 @@ struct ContentView: View {
163163 }
164164
165165 private func isInlineUserList( _ filter: FilterList ) -> Bool {
166- filter. isCustom
167- && filter. url. scheme? . lowercased ( ) == " wblock "
166+ filter. url. scheme? . lowercased ( ) == " wblock "
168167 && filter. url. host? . lowercased ( ) == " userlist "
169168 }
170169
170+ private func supportsCustomActions( _ filter: FilterList ) -> Bool {
171+ filter. isCustom || filterManager. customFilterLists. contains ( where: { $0. id == filter. id } )
172+ }
173+
171174 private var filtersView : some View {
172175 NavigationStack {
173- ScrollView {
174- VStack ( spacing: 20 ) {
175- statsCardsView
176-
177- VStack ( spacing: 16 ) {
178- ForEach ( categorizedFilters, id: \. category) { item in
179- if item. category == . foreign {
180- foreignFiltersDisclosureView ( filters: item. filters)
181- } else {
182- filterSectionView ( category: item. category, filters: item. filters)
183- }
184- }
185- }
186- . padding ( . horizontal)
187-
188- Spacer ( minLength: 20 )
189- }
190- . padding ( . vertical)
191- }
192- #if os(iOS)
193- . padding( . horizontal, 16 )
176+ nativeFiltersListView
177+ #if os(iOS)
194178 . navigationBarTitleDisplayMode( . inline)
195179 . toolbar {
196180 ToolbarItem ( placement: . topBarLeading) {
@@ -279,6 +263,77 @@ struct ContentView: View {
279263 #endif
280264 }
281265
266+ private var nativeFiltersListView : some View {
267+ #if os(iOS)
268+ List {
269+ Section {
270+ statsCardsView
271+ . unifiedTabCardSectionRow ( )
272+ }
273+
274+ ForEach ( categorizedFilters, id: \. category) { item in
275+ if item. category == . foreign {
276+ Section {
277+ if dataManager. isForeignFiltersExpanded {
278+ ForEach ( item. filters) { filter in
279+ filterRowView ( for: filter)
280+ }
281+ }
282+ } header: {
283+ Button {
284+ Task {
285+ await dataManager. setIsForeignFiltersExpanded (
286+ !dataManager. isForeignFiltersExpanded
287+ )
288+ }
289+ } label: {
290+ HStack {
291+ Text ( item. category. localizedName)
292+ Spacer ( )
293+ Image (
294+ systemName: dataManager. isForeignFiltersExpanded
295+ ? " chevron.down "
296+ : " chevron.right "
297+ )
298+ . font ( . caption)
299+ }
300+ }
301+ . buttonStyle ( . plain)
302+ . textCase ( nil )
303+ }
304+ } else {
305+ Section ( item. category. localizedName) {
306+ ForEach ( item. filters) { filter in
307+ filterRowView ( for: filter)
308+ }
309+ }
310+ }
311+ }
312+ }
313+ . unifiedTabListStyle ( )
314+ #else
315+ ScrollView {
316+ VStack ( spacing: 20 ) {
317+ statsCardsView
318+
319+ VStack ( spacing: 16 ) {
320+ ForEach ( categorizedFilters, id: \. category) { item in
321+ if item. category == . foreign {
322+ macOSForeignFiltersView ( filters: item. filters)
323+ } else {
324+ macOSFilterSectionView ( category: item. category, filters: item. filters)
325+ }
326+ }
327+ }
328+ . padding ( . horizontal)
329+
330+ Spacer ( minLength: 20 )
331+ }
332+ . padding ( . vertical)
333+ }
334+ #endif
335+ }
336+
282337 private var userscriptsView : some View {
283338 NavigationStack {
284339 UserScriptManagerView ( userScriptManager: userScriptManager)
@@ -306,94 +361,81 @@ struct ContentView: View {
306361
307362 private var statsCardsView : some View {
308363 HStack ( spacing: 12 ) {
309- #if os(iOS)
310- Button {
311- filterManager. showRuleLimitWarning ( )
312- } label: {
313- StatCard (
314- title: " Rules " ,
315- value: enabledListsCount == 0
316- ? " 0 "
317- : ( hasAppliedFilters
318- ? appliedSafariRulesCount. formatted ( )
319- : ( sourceRulesCount > 0 ? " ~ \( sourceRulesCount. formatted ( ) ) " : " 0 " ) ) ,
320- icon: " shield.lefthalf.filled " ,
321- pillColor: . clear,
322- valueColor: enabledListsCount == 0 ? . secondary : ( hasAppliedFilters ? . primary : . secondary)
323- )
324- . overlay ( alignment: . topTrailing) {
325- if shouldShowRuleLimitIndicator {
326- Image ( systemName: " exclamationmark.triangle.fill " )
327- . font ( . caption2)
328- . foregroundStyle ( . orange)
329- . padding ( . trailing, 10 )
330- . padding ( . top, 8 )
331- }
332- }
333- }
334- . buttonStyle ( . plain)
335- #else
336- Button {
337- filterManager. showRuleLimitWarning ( )
338- } label: {
339- StatCard (
340- title: ( enabledListsCount == 0 || !hasAppliedFilters) ? " Source Rules " : " Safari Rules " ,
341- value: enabledListsCount == 0
342- ? " 0 "
343- : ( hasAppliedFilters
344- ? appliedSafariRulesCount. formatted ( )
345- : ( sourceRulesCount > 0 ? " ~ \( sourceRulesCount. formatted ( ) ) " : " 0 " ) ) ,
346- icon: " shield.lefthalf.filled " ,
347- pillColor: . clear,
348- valueColor: enabledListsCount == 0 ? . secondary : ( hasAppliedFilters ? . primary : . secondary)
349- )
350- . overlay ( alignment: . topTrailing) {
351- if shouldShowRuleLimitIndicator {
352- Image ( systemName: " exclamationmark.triangle.fill " )
353- . font ( . caption2)
354- . foregroundStyle ( . orange)
355- . padding ( . trailing, 10 )
356- . padding ( . top, 8 )
357- }
364+ Button {
365+ filterManager. showRuleLimitWarning ( )
366+ } label: {
367+ StatCard (
368+ title: {
369+ #if os(iOS)
370+ return " Rules "
371+ #else
372+ return ( enabledListsCount == 0 || !hasAppliedFilters) ? " Source Rules " : " Safari Rules "
373+ #endif
374+ } ( ) ,
375+ value: enabledListsCount == 0
376+ ? " 0 "
377+ : ( hasAppliedFilters
378+ ? appliedSafariRulesCount. formatted ( )
379+ : ( sourceRulesCount > 0 ? " ~ \( sourceRulesCount. formatted ( ) ) " : " 0 " ) ) ,
380+ icon: " shield.lefthalf.filled " ,
381+ pillColor: . clear,
382+ valueColor: enabledListsCount == 0 ? . secondary : ( hasAppliedFilters ? . primary : . secondary)
383+ )
384+ . overlay ( alignment: . topTrailing) {
385+ if shouldShowRuleLimitIndicator {
386+ Image ( systemName: " exclamationmark.triangle.fill " )
387+ . font ( . caption2)
388+ . foregroundStyle ( . orange)
389+ . padding ( . trailing, 6 )
390+ . padding ( . top, 4 )
358391 }
359392 }
360- . buttonStyle ( . plain)
361- #endif
393+ #if os(iOS)
394+ . frame( maxWidth: . infinity, alignment: . leading)
395+ #endif
396+ }
397+ . buttonStyle ( . plain)
398+
362399 StatCard (
363400 title: " Enabled Lists " ,
364401 value: " \( enabledListsCount) " ,
365402 icon: " list.bullet.rectangle " ,
366403 pillColor: . clear,
367404 valueColor: . primary
368405 )
406+ #if os(iOS)
407+ . frame( maxWidth: . infinity, alignment: . leading)
408+ #endif
369409 }
370410 . padding ( . horizontal)
371411 }
372412
373- private func filterSectionView( category: FilterListCategory , filters: [ FilterList ] ) -> some View
374- {
413+ private func filterRowView( for filter: FilterList ) -> some View {
414+ FilterRowView (
415+ filter: filter,
416+ isInlineUserList: isInlineUserList ( filter) ,
417+ supportsCustomActions: supportsCustomActions ( filter) ,
418+ onEdit: { editingCustomFilter = filter } ,
419+ onDelete: { filterManager. removeFilterList ( filter) } ,
420+ onToggle: { _ in filterManager. toggleFilterListSelection ( id: filter. id) } ,
421+ onShowRuleLimitWarning: { filterManager. showRuleLimitWarning ( for: filter) }
422+ )
423+ }
424+
425+ #if os(macOS)
426+ private func macOSFilterSectionView( category: FilterListCategory , filters: [ FilterList ] ) -> some View {
375427 VStack ( alignment: . leading, spacing: 12 ) {
376428 HStack {
377429 Text ( category. localizedName)
378430 . font ( . headline)
379431 . foregroundColor ( . primary)
380-
381432 Spacer ( )
382433 }
383434 . padding ( . horizontal, 4 )
384435
385436 VStack ( spacing: 0 ) {
386437 ForEach ( filters) { filter in
387- FilterRowView (
388- filter: filter,
389- isInlineUserList: isInlineUserList ( filter) ,
390- onEdit: { editingCustomFilter = filter } ,
391- onDelete: { filterManager. removeFilterList ( filter) } ,
392- onToggle: { _ in filterManager. toggleFilterListSelection ( id: filter. id) } ,
393- onShowRuleLimitWarning: { filterManager. showRuleLimitWarning ( for: filter) }
394- )
395- . equatable ( )
396-
438+ filterRowView ( for: filter)
397439 if filter. id != filters. last? . id {
398440 Divider ( )
399441 . padding ( . leading, 16 )
@@ -404,9 +446,8 @@ struct ContentView: View {
404446 }
405447 }
406448
407- private func foreignFiltersDisclosureView ( filters: [ FilterList ] ) -> some View {
449+ private func macOSForeignFiltersView ( filters: [ FilterList ] ) -> some View {
408450 VStack ( alignment: . leading, spacing: 12 ) {
409- // Manual expand/collapse button instead of DisclosureGroup (avoids LazyVStack bug)
410451 Button {
411452 Task {
412453 await dataManager. setIsForeignFiltersExpanded (
@@ -417,9 +458,7 @@ struct ContentView: View {
417458 Text ( FilterListCategory . foreign. localizedName)
418459 . font ( . headline)
419460 . foregroundColor ( . primary)
420-
421461 Spacer ( )
422-
423462 Image (
424463 systemName: dataManager. isForeignFiltersExpanded
425464 ? " chevron.down " : " chevron.right "
@@ -434,16 +473,7 @@ struct ContentView: View {
434473 if dataManager. isForeignFiltersExpanded {
435474 VStack ( spacing: 0 ) {
436475 ForEach ( filters) { filter in
437- FilterRowView (
438- filter: filter,
439- isInlineUserList: isInlineUserList ( filter) ,
440- onEdit: { editingCustomFilter = filter } ,
441- onDelete: { filterManager. removeFilterList ( filter) } ,
442- onToggle: { _ in filterManager. toggleFilterListSelection ( id: filter. id) } ,
443- onShowRuleLimitWarning: { filterManager. showRuleLimitWarning ( for: filter) }
444- )
445- . equatable ( )
446-
476+ filterRowView ( for: filter)
447477 if filter. id != filters. last? . id {
448478 Divider ( )
449479 . padding ( . leading, 16 )
@@ -454,23 +484,48 @@ struct ContentView: View {
454484 }
455485 }
456486 }
457-
487+ #endif
458488}
459489
460- struct FilterRowView : View , Equatable {
490+ struct FilterRowView : View {
461491 let filter : FilterList
462492 let isInlineUserList : Bool
493+ let supportsCustomActions : Bool
463494 var onEdit : ( ) -> Void
464495 var onDelete : ( ) -> Void
465496 var onToggle : ( Bool ) -> Void
466497 var onShowRuleLimitWarning : ( ) -> Void
467498
468- static func == ( lhs: FilterRowView , rhs: FilterRowView ) -> Bool {
469- lhs. filter == rhs. filter && lhs. isInlineUserList == rhs. isInlineUserList
499+ @ViewBuilder
500+ private var contextMenuItems : some View {
501+ if supportsCustomActions {
502+ Button {
503+ onEdit ( )
504+ } label: {
505+ Label ( isInlineUserList ? " Edit Rules " : " Edit Name " , systemImage: " pencil " )
506+ }
507+
508+ Button ( role: . destructive) {
509+ onDelete ( )
510+ } label: {
511+ Label ( " Delete Added List " , systemImage: " trash " )
512+ }
513+ }
514+
515+ Button {
516+ #if os(macOS)
517+ NSPasteboard . general. clearContents ( )
518+ NSPasteboard . general. setString ( filter. url. absoluteString, forType: . string)
519+ #else
520+ UIPasteboard . general. string = filter. url. absoluteString
521+ #endif
522+ } label: {
523+ Label ( " Copy URL " , systemImage: " doc.on.doc " )
524+ }
470525 }
471526
472527 var body : some View {
473- HStack ( alignment: . top, spacing: 12 ) {
528+ HStack ( alignment: . top, spacing: 10 ) {
474529 VStack ( alignment: . leading, spacing: 4 ) {
475530 HStack ( spacing: 6 ) {
476531 if let flags = filter. flagEmojis {
@@ -570,34 +625,12 @@ struct FilterRowView: View, Equatable {
570625 . toggleStyle ( . switch)
571626 . frame ( alignment: . center)
572627 }
573- . padding ( 16 )
574- . id ( filter. id)
575- . contentShape ( . interaction, Rectangle ( ) )
576628 . contextMenu {
577- if filter. isCustom {
578- Button {
579- onEdit ( )
580- } label: {
581- Label ( isInlineUserList ? " Edit Rules " : " Edit Name " , systemImage: " pencil " )
582- }
583-
584- Button ( role: . destructive) {
585- onDelete ( )
586- } label: {
587- Label ( " Delete Added List " , systemImage: " trash " )
588- }
589- }
590- Button {
591- #if os(macOS)
592- NSPasteboard . general. clearContents ( )
593- NSPasteboard . general. setString ( filter. url. absoluteString, forType: . string)
594- #else
595- UIPasteboard . general. string = filter. url. absoluteString
596- #endif
597- } label: {
598- Label ( " Copy URL " , systemImage: " doc.on.doc " )
599- }
629+ contextMenuItems
600630 }
631+ #if os(macOS)
632+ . padding( 16 )
633+ #endif
601634 }
602635}
603636
0 commit comments