diff --git a/Palace.xcodeproj/project.pbxproj b/Palace.xcodeproj/project.pbxproj index 537bea4ec..3c2cdc435 100644 --- a/Palace.xcodeproj/project.pbxproj +++ b/Palace.xcodeproj/project.pbxproj @@ -4676,7 +4676,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.35; + MARKETING_VERSION = 1.0.36; PRODUCT_BUNDLE_IDENTIFIER = org.thepalaceproject.palace; PRODUCT_MODULE_NAME = Palace; PRODUCT_NAME = "Palace-noDRM"; @@ -4733,7 +4733,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.35; + MARKETING_VERSION = 1.0.36; PRODUCT_BUNDLE_IDENTIFIER = org.thepalaceproject.palace; PRODUCT_MODULE_NAME = Palace; PRODUCT_NAME = "Palace-noDRM"; diff --git a/Palace/Reader2/UI/EpubSearchView/EPUBSearchView.swift b/Palace/Reader2/UI/EpubSearchView/EPUBSearchView.swift index 27a1d2707..3cf375c01 100644 --- a/Palace/Reader2/UI/EpubSearchView/EPUBSearchView.swift +++ b/Palace/Reader2/UI/EpubSearchView/EPUBSearchView.swift @@ -21,7 +21,7 @@ struct EPUBSearchView: View { @ViewBuilder private var searchBar: some View { HStack { TextField("\(Strings.Generic.search)...", text: $searchQuery) - .focused($isSearchFieldFocused) // Bind the focus state to the text field + .focused($isSearchFieldFocused) Button(action: { searchQuery = "" viewModel.cancelSearch() @@ -37,7 +37,6 @@ struct EPUBSearchView: View { .padding(.bottom) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - // This delay ensures that the view is fully loaded before focusing isSearchFieldFocused = true } } @@ -56,9 +55,9 @@ struct EPUBSearchView: View { @ViewBuilder private var listView: some View { ZStack { List { - ForEach(groupedByChapterName(viewModel.results), id: \.key) { key, locators in - Section(header: sectionHeaderView(title: key)) { - ForEach(locators, id: \.href) { locator in + ForEach(viewModel.groupedResults, id: \.title) { section in + Section(header: sectionHeaderView(title: section.title)) { + ForEach(section.locators, id: \.self) { locator in rowView(locator) .onAppear(perform: { if shouldFetchMoreResults(for: locator) { @@ -82,28 +81,13 @@ struct EPUBSearchView: View { } } } - - private func shouldFetchMoreResults(for locator: Locator) -> Bool { - viewModel.results.last?.href == locator.href - } - private func groupedByChapterName(_ results: [Locator]) -> [(key: String, value: [Locator])] { - let hasTitles = results.contains { $0.title != nil && $0.title != "" } - - if !hasTitles { - return [("", results)] - } - - let uniqueTitles = Array(Set(results.compactMap { $0.title })).sorted { title1, title2 in - results.firstIndex(where: { $0.title == title1 })! < results.firstIndex(where: { $0.title == title2 })! - } - - return uniqueTitles.compactMap { title -> (key: String, value: [Locator])? in - if let items = results.filter({ $0.title == title }) as [Locator]?, !items.isEmpty { - return (key: title, value: items) - } - return nil + private func shouldFetchMoreResults(for locator: Locator) -> Bool { + if let lastSection = viewModel.groupedResults.last, + let lastLocator = lastSection.locators.last { + return locator.href == lastLocator.href } + return false } private func sectionHeaderView(title: String) -> some View { @@ -123,7 +107,7 @@ struct EPUBSearchView: View { EmptyView() } } - + private func rowView(_ locator: Locator) -> some View { let text = locator.text.sanitized() diff --git a/Palace/Reader2/UI/EpubSearchView/EPUBSearchViewModel.swift b/Palace/Reader2/UI/EpubSearchView/EPUBSearchViewModel.swift index 3b22eb2b0..919051288 100644 --- a/Palace/Reader2/UI/EpubSearchView/EPUBSearchViewModel.swift +++ b/Palace/Reader2/UI/EpubSearchView/EPUBSearchViewModel.swift @@ -35,7 +35,8 @@ final class EPUBSearchViewModel: ObservableObject { @Published private(set) var state: State = .empty @Published private(set) var results: [Locator] = [] - + @Published private(set) var groupedResults: [(title: String, locators: [Locator])] = [] + private var publication: Publication weak var delegate: EPUBSearchDelegate? @@ -58,26 +59,20 @@ final class EPUBSearchViewModel: ObservableObject { state = .starting(cancellable) } - + func fetchNextBatch() { - guard case let .idle(iterator, _) = state else { return } + guard case let .idle(iterator, _) = state else { + return + } state = .loadingNext(iterator, nil) - let cancellable = iterator.next { result in + let cancellable = iterator.next { [weak self] result in + guard let self = self else { return } + switch result { case .success(let collection): - if let collection = collection { - for locator in collection.locators { - if !self.results.contains(where: { $0.href == locator.href }) { - self.results.append(locator) - } - } - self.state = .idle(iterator, isFetching: false) - } else { - self.state = .end - } - + self.handleNewCollection(iterator, collection: collection) case .failure(let error): self.state = .failure(error) } @@ -86,7 +81,58 @@ final class EPUBSearchViewModel: ObservableObject { state = .loadingNext(iterator, cancellable) } + private func handleNewCollection(_ iterator: SearchIterator, collection: _LocatorCollection?) { + guard let collection = collection else { + state = .end + return + } + + for newLocator in collection.locators { + if !isDuplicate(newLocator) { + self.results.append(newLocator) + } + } + + groupResults() + + self.state = .idle(iterator, isFetching: false) + } + + private func groupResults() { + var groupedResults: [String: [Locator]] = [:] + + for locator in results { + let titleKey = locator.title ?? locator.href + + if !groupedResults.keys.contains(titleKey) { + groupedResults[titleKey] = [] + } + + if !groupedResults[titleKey]!.contains(where: { existingLocator in + existingLocator.href == locator.href && + existingLocator.locations.progression == locator.locations.progression && + existingLocator.locations.totalProgression == locator.locations.totalProgression + }) { + groupedResults[titleKey]!.append(locator) + } + } + + self.groupedResults = groupedResults + .map { (title: $0.value.first?.title ?? "", locators: $0.value) } + .sorted { section1, section2 in + let href1 = section1.locators.first?.href.split(separator: "/").dropFirst(2).joined(separator: "/") ?? "" + let href2 = section2.locators.first?.href.split(separator: "/").dropFirst(2).joined(separator: "/") ?? "" + return href1 < href2 + } + } + private func isDuplicate(_ locator: Locator) -> Bool { + return self.results.contains { existingLocator in + existingLocator.href == locator.href && + existingLocator.locations.progression == locator.locations.progression && + existingLocator.locations.totalProgression == locator.locations.totalProgression + } + } func cancelSearch() { switch state {