summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch.xcodeproj/project.pbxproj16
-rw-r--r--Hutch/Views/Builds/BuildListView.swift1
-rw-r--r--Hutch/Views/Builds/BuildListViewModel.swift35
-rw-r--r--Hutch/Views/Builds/BuildRowView.swift2
-rw-r--r--Hutch/Views/Pastes/PasteListView.swift3
-rw-r--r--Hutch/Views/Pastes/PasteListViewModel.swift28
-rw-r--r--Hutch/Views/Tickets/TicketListView.swift6
-rw-r--r--Hutch/Views/Tickets/TicketListViewModel.swift20
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