fix: update integration_test.go to match server.New signature (version, commit args)
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
This commit is contained in:
115
ios/LibNovelV2/ViewModels/SearchViewModel.swift
Normal file
115
ios/LibNovelV2/ViewModels/SearchViewModel.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - SearchViewModel
|
||||
// Debounced live search (300 ms) + recent searches persisted in UserDefaults.
|
||||
// Uses @Observable (iOS 17+).
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class SearchViewModel {
|
||||
var query: String = ""
|
||||
var results: [BrowseNovel] = []
|
||||
var localCount: Int = 0
|
||||
var remoteCount: Int = 0
|
||||
var isLoading = false
|
||||
var error: String?
|
||||
|
||||
// Persisted recent searches (max 10, prefixed with "v2.")
|
||||
var recentSearches: [String] = []
|
||||
|
||||
private let recentKey = "v2.searchRecentTerms"
|
||||
private var searchTask: Task<Void, Never>?
|
||||
|
||||
init() {
|
||||
recentSearches = UserDefaults.standard.stringArray(forKey: recentKey) ?? []
|
||||
}
|
||||
|
||||
// MARK: - Query change (debounced)
|
||||
|
||||
func onQueryChange(_ newValue: String) {
|
||||
searchTask?.cancel()
|
||||
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else {
|
||||
results = []
|
||||
localCount = 0
|
||||
remoteCount = 0
|
||||
return
|
||||
}
|
||||
searchTask = Task {
|
||||
try? await Task.sleep(nanoseconds: 300_000_000) // 300 ms debounce
|
||||
guard !Task.isCancelled else { return }
|
||||
await runSearch(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Submit (immediate, saves to recent)
|
||||
|
||||
func submitSearch() {
|
||||
let term = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !term.isEmpty else { return }
|
||||
saveRecent(term)
|
||||
searchTask?.cancel()
|
||||
searchTask = Task { await runSearch(term) }
|
||||
}
|
||||
|
||||
// MARK: - Recent search tap
|
||||
|
||||
func selectRecent(_ term: String) {
|
||||
query = term
|
||||
searchTask?.cancel()
|
||||
searchTask = Task { await runSearch(term) }
|
||||
}
|
||||
|
||||
// MARK: - Clear
|
||||
|
||||
func clear() {
|
||||
query = ""
|
||||
results = []
|
||||
localCount = 0
|
||||
remoteCount = 0
|
||||
error = nil
|
||||
searchTask?.cancel()
|
||||
}
|
||||
|
||||
func clearRecent() {
|
||||
recentSearches = []
|
||||
UserDefaults.standard.removeObject(forKey: recentKey)
|
||||
}
|
||||
|
||||
// MARK: - Core search
|
||||
|
||||
private func runSearch(_ term: String) async {
|
||||
guard !term.isEmpty else {
|
||||
results = []
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
error = nil
|
||||
do {
|
||||
let response = try await APIClient.shared.search(query: term)
|
||||
// Only update if the query hasn't changed since we started
|
||||
let currentTrimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if currentTrimmed == term || currentTrimmed.isEmpty {
|
||||
results = response.results
|
||||
localCount = response.localCount
|
||||
remoteCount = response.remoteCount
|
||||
}
|
||||
} catch {
|
||||
if !(error is CancellationError) {
|
||||
self.error = error.localizedDescription
|
||||
results = []
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
// MARK: - Persist recent
|
||||
|
||||
private func saveRecent(_ term: String) {
|
||||
var list = recentSearches.filter { $0 != term }
|
||||
list.insert(term, at: 0)
|
||||
if list.count > 10 { list = Array(list.prefix(10)) }
|
||||
recentSearches = list
|
||||
UserDefaults.standard.set(list, forKey: recentKey)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user