Skip to content

Commit d8addcc

Browse files
committed
native UI overhaul: iOS uses List/Section, macOS uses ScrollView with material cards
1 parent 3d03b8a commit d8addcc

5 files changed

Lines changed: 684 additions & 560 deletions

File tree

wBlock/ContentView.swift

Lines changed: 165 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)