Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PP-725] Improve epub search #360

Merged
merged 3 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Palace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -4654,7 +4654,7 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 218;
CURRENT_PROJECT_VERSION = 219;
DEVELOPMENT_TEAM = 88CBA74T8K;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 88CBA74T8K;
ENABLE_BITCODE = NO;
Expand All @@ -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";
Expand Down Expand Up @@ -4711,7 +4711,7 @@
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR;
CODE_SIGN_ENTITLEMENTS = Palace/SimplyE.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 218;
CURRENT_PROJECT_VERSION = 219;
DEVELOPMENT_TEAM = 88CBA74T8K;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 88CBA74T8K;
ENABLE_BITCODE = NO;
Expand All @@ -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";
Expand Down Expand Up @@ -4895,7 +4895,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 218;
CURRENT_PROJECT_VERSION = 219;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 88CBA74T8K;
ENABLE_BITCODE = NO;
Expand Down Expand Up @@ -4955,7 +4955,7 @@
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR;
CODE_SIGN_ENTITLEMENTS = Palace/SimplyE.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 218;
CURRENT_PROJECT_VERSION = 219;
DEVELOPMENT_TEAM = 88CBA74T8K;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 88CBA74T8K;
ENABLE_BITCODE = NO;
Expand Down
36 changes: 10 additions & 26 deletions Palace/Reader2/UI/EpubSearchView/EPUBSearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
}
}
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -123,7 +107,7 @@ struct EPUBSearchView: View {
EmptyView()
}
}

private func rowView(_ locator: Locator) -> some View {
let text = locator.text.sanitized()

Expand Down
76 changes: 61 additions & 15 deletions Palace/Reader2/UI/EpubSearchView/EPUBSearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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)
}
Expand All @@ -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 {
Expand Down