From b3d3e68e742650cb8a448a1f51fea7fb7783c36f Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sun, 12 Apr 2026 18:59:34 -0500 Subject: feat: add builds auto-refresh and repo filtering implements: https://todo.sr.ht/~ccleberg/hutch/17 implements: https://todo.sr.ht/~ccleberg/hutch/18 --- Hutch/Views/Builds/BuildListViewModel.swift | 66 +++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) (limited to 'Hutch/Views/Builds/BuildListViewModel.swift') diff --git a/Hutch/Views/Builds/BuildListViewModel.swift b/Hutch/Views/Builds/BuildListViewModel.swift index 8468cff..da29ca5 100644 --- a/Hutch/Views/Builds/BuildListViewModel.swift +++ b/Hutch/Views/Builds/BuildListViewModel.swift @@ -25,6 +25,20 @@ enum BuildListFilter: String, CaseIterable, Sendable { case all = "All" } +enum AutoRefreshInterval: Int, CaseIterable, Sendable { + case off = 0 + case fiveSeconds = 5 + case tenSeconds = 10 + + var label: String { + switch self { + case .off: "Off" + case .fiveSeconds: "5s" + case .tenSeconds: "10s" + } + } +} + // MARK: - View Model @Observable @@ -39,10 +53,15 @@ final class BuildListViewModel { var error: String? var filter: BuildListFilter = .attention var searchText = "" + var repoFilter: String = "" { + didSet { if repoFilter != oldValue { repoFilterDidChange() } } + } private var cursor: String? private var hasMore = true private let client: SRHTClient + private var refreshTask: Task? + private var isAutoRefreshing = false private static let cacheKey = "builds.jobs" @@ -50,11 +69,22 @@ final class BuildListViewModel { self.client = client } + /// Unique tags across all loaded jobs, sorted alphabetically. + var availableTags: [String] { + let allTags = Set(jobs.flatMap(\.tags)) + return allTags.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } + } + var filteredJobs: [JobSummary] { - let statusFiltered = Self.filterJobs(jobs, filter: filter) + var result = Self.filterJobs(jobs, filter: filter) + + if !repoFilter.isEmpty { + result = result.filter { $0.tags.contains(repoFilter) } + } + let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard !q.isEmpty else { return statusFiltered } - return statusFiltered.filter { + guard !q.isEmpty else { return result } + return result.filter { String($0.id).contains(q) || $0.tags.contains { $0.lowercased().contains(q) } || ($0.note?.lowercased().contains(q) == true) || @@ -62,6 +92,36 @@ final class BuildListViewModel { } } + // MARK: - Auto-Refresh + + func startAutoRefresh(interval: AutoRefreshInterval) { + stopAutoRefresh() + guard interval != .off else { return } + let seconds = interval.rawValue + refreshTask = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(seconds)) + guard !Task.isCancelled, let self else { return } + guard !self.isAutoRefreshing, !self.isLoading, !self.isRefreshing else { continue } + self.isAutoRefreshing = true + await self.loadJobs() + self.isAutoRefreshing = false + } + } + } + + func stopAutoRefresh() { + refreshTask?.cancel() + refreshTask = nil + } + + private func repoFilterDidChange() { + // Reset to empty if the selected tag no longer exists + if !repoFilter.isEmpty, !availableTags.contains(repoFilter) { + repoFilter = "" + } + } + // MARK: - Query private static let query = """ -- cgit v1.2.3