diff options
| -rw-r--r-- | Hutch.xcodeproj/project.pbxproj | 16 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildListView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildListViewModel.swift | 35 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildRowView.swift | 2 | ||||
| -rw-r--r-- | Hutch/Views/Pastes/PasteListView.swift | 3 | ||||
| -rw-r--r-- | Hutch/Views/Pastes/PasteListViewModel.swift | 28 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TicketListView.swift | 6 | ||||
| -rw-r--r-- | Hutch/Views/Tickets/TicketListViewModel.swift | 20 |
8 files changed, 78 insertions, 33 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index f2a08a8..e8b6b96 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -532,7 +532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -552,7 +552,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -632,7 +632,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -642,7 +642,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -661,7 +661,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 61; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -671,7 +671,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.0.0; + MARKETING_VERSION = 3.0.1; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift index 6f3f934..d525918 100644 --- a/Hutch/Views/Builds/BuildListView.swift +++ b/Hutch/Views/Builds/BuildListView.swift @@ -137,6 +137,7 @@ struct BuildListView: View { ForEach(viewModel.filteredJobs) { job in NavigationLink(value: job) { BuildRowView(job: job) + .equatable() } .contextMenu { Button { diff --git a/Hutch/Views/Builds/BuildListViewModel.swift b/Hutch/Views/Builds/BuildListViewModel.swift index c6f8e49..643bacd 100644 --- a/Hutch/Views/Builds/BuildListViewModel.swift +++ b/Hutch/Views/Builds/BuildListViewModel.swift @@ -46,18 +46,33 @@ enum AutoRefreshInterval: Int, CaseIterable, Sendable { final class BuildListViewModel { private static let searchHistoryScopeID = "builds" - private(set) var jobs: [JobSummary] = [] + private(set) var jobs: [JobSummary] = [] { + didSet { updateFilteredJobs() } + } private(set) var recentSearches: [ScopedSearchHistoryEntry] private(set) var isLoading = false private(set) var isLoadingMore = false private(set) var isRefreshing = false private(set) var isSubmitting = false var error: String? - var filter: BuildListFilter = .attention - var searchText = "" + var filter: BuildListFilter = .attention { + didSet { updateFilteredJobs() } + } + var searchText = "" { + didSet { updateFilteredJobs() } + } var repoFilter: String = "" { - didSet { if repoFilter != oldValue { repoFilterDidChange() } } + didSet { + guard repoFilter != oldValue else { return } + repoFilterDidChange() + updateFilteredJobs() + } } + // Cached filtered result. Updated whenever jobs, filter, searchText, or + // repoFilter changes. Only notifies observers when the content actually + // differs, which prevents the list from re-rendering on auto-refresh when + // no visible data changed. + private(set) var filteredJobs: [JobSummary] = [] private var cursor: String? private var hasMore = true @@ -83,14 +98,18 @@ final class BuildListViewModel { return allTags.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } } - var filteredJobs: [JobSummary] { + private func updateFilteredJobs() { var result = Self.filterJobs(jobs, filter: filter) - if !repoFilter.isEmpty { result = result.filter { $0.tags.contains(repoFilter) } } - - return Self.searchJobs(result, matching: searchText) + let updated = Self.searchJobs(result, matching: searchText) + // Skip the assignment (and the resulting observer notification) when the + // filtered list hasn't actually changed — e.g. on auto-refresh when no + // builds have been added or updated. + if updated != filteredJobs { + filteredJobs = updated + } } // MARK: - Auto-Refresh diff --git a/Hutch/Views/Builds/BuildRowView.swift b/Hutch/Views/Builds/BuildRowView.swift index 9602e5c..a490123 100644 --- a/Hutch/Views/Builds/BuildRowView.swift +++ b/Hutch/Views/Builds/BuildRowView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct BuildRowView: View { +struct BuildRowView: View, Equatable { let job: JobSummary var body: some View { diff --git a/Hutch/Views/Pastes/PasteListView.swift b/Hutch/Views/Pastes/PasteListView.swift index 0fdddd7..9363b08 100644 --- a/Hutch/Views/Pastes/PasteListView.swift +++ b/Hutch/Views/Pastes/PasteListView.swift @@ -73,6 +73,7 @@ struct PasteListView: View { ForEach(viewModel.filteredPastes) { paste in NavigationLink(value: paste) { PasteRowView(paste: paste) + .equatable() } .swipeActions(edge: .leading, allowsFullSwipe: true) { if swipeActionsEnabled { @@ -208,7 +209,7 @@ struct PasteListView: View { } } -private struct PasteRowView: View { +private struct PasteRowView: View, Equatable { let paste: Paste var body: some View { diff --git a/Hutch/Views/Pastes/PasteListViewModel.swift b/Hutch/Views/Pastes/PasteListViewModel.swift index b521e18..75f2f4b 100644 --- a/Hutch/Views/Pastes/PasteListViewModel.swift +++ b/Hutch/Views/Pastes/PasteListViewModel.swift @@ -3,13 +3,18 @@ import Foundation @Observable @MainActor final class PasteListViewModel { - private(set) var pastes: [Paste] = [] + private(set) var pastes: [Paste] = [] { + didSet { updateFilteredPastes() } + } private(set) var isLoading = false private(set) var isLoadingMore = false private(set) var isRefreshing = false private(set) var isCreatingPaste = false var error: String? - var searchText = "" + var searchText = "" { + didSet { updateFilteredPastes() } + } + private(set) var filteredPastes: [Paste] = [] private var cursor: String? private var hasMore = true @@ -19,15 +24,22 @@ final class PasteListViewModel { self.service = service } - var filteredPastes: [Paste] { + private func updateFilteredPastes() { let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard !q.isEmpty else { return pastes } - return pastes.filter { - $0.files.contains { - ($0.filename?.lowercased().contains(q) == true) || - $0.hash.lowercased().hasPrefix(q) + let updated: [Paste] + if q.isEmpty { + updated = pastes + } else { + updated = pastes.filter { + $0.files.contains { + ($0.filename?.lowercased().contains(q) == true) || + $0.hash.lowercased().hasPrefix(q) + } } } + if updated != filteredPastes { + filteredPastes = updated + } } func loadPastes() async { diff --git a/Hutch/Views/Tickets/TicketListView.swift b/Hutch/Views/Tickets/TicketListView.swift index 8f7d348..e58d194 100644 --- a/Hutch/Views/Tickets/TicketListView.swift +++ b/Hutch/Views/Tickets/TicketListView.swift @@ -290,6 +290,7 @@ struct TicketListView: View { ticket: ticket, isSelected: viewModel.selectedTicketIDs.contains(ticket.id) ) + .equatable() } .buttonStyle(.plain) } else { @@ -303,6 +304,7 @@ struct TicketListView: View { ) } label: { TicketRowView(ticket: ticket) + .equatable() } .contextMenu { if let url = SRHTWebURL.ticket(ownerUsername: ownerUsername(for: tracker), trackerName: tracker.name, ticketId: ticket.id) { @@ -1180,7 +1182,7 @@ private struct BulkAssignSheet: View { } } -private struct SelectableTicketRow: View { +private struct SelectableTicketRow: View, Equatable { let ticket: TicketSummary let isSelected: Bool @@ -1200,7 +1202,7 @@ private struct SelectableTicketRow: View { // MARK: - Ticket Row -private struct TicketRowView: View { +private struct TicketRowView: View, Equatable { let ticket: TicketSummary var body: some View { diff --git a/Hutch/Views/Tickets/TicketListViewModel.swift b/Hutch/Views/Tickets/TicketListViewModel.swift index e7cc565..e23cc52 100644 --- a/Hutch/Views/Tickets/TicketListViewModel.swift +++ b/Hutch/Views/Tickets/TicketListViewModel.swift @@ -83,7 +83,9 @@ final class TicketListViewModel { let trackerId: Int let trackerRid: String - private(set) var tickets: [TicketSummary] = [] + private(set) var tickets: [TicketSummary] = [] { + didSet { updateFilteredTickets() } + } private(set) var isLoading = false private(set) var isLoadingMore = false private(set) var isCreatingTicket = false @@ -97,15 +99,21 @@ final class TicketListViewModel { var filter: TicketFilter = .open { didSet { persistFilterState() + updateFilteredTickets() } } var selectedLabelIDs: Set<Int> = [] { didSet { persistFilterState() + updateFilteredTickets() } } - var searchText = "" + var searchText = "" { + didSet { updateFilteredTickets() } + } private(set) var activeSavedFilterID: SavedTicketFilter.ID? + // Cached filtered result. See updateFilteredTickets(). + private(set) var filteredTickets: [TicketSummary] = [] private var cursor: String? private var hasMore = true @@ -268,9 +276,11 @@ final class TicketListViewModel { !currentFilterState.isDefault } - /// Tickets filtered by the selected status and label filters. - var filteredTickets: [TicketSummary] { - Self.filterTickets(tickets, state: currentFilterState, query: searchText) + private func updateFilteredTickets() { + let updated = Self.filterTickets(tickets, state: currentFilterState, query: searchText) + if updated != filteredTickets { + filteredTickets = updated + } } // MARK: - Public API |
