From 05331031c5999925f2f9855dbb8c251cd8ffaa55 Mon Sep 17 00:00:00 2001 From: ChrisLi <8294252+automactic@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:34:02 -0400 Subject: [PATCH] Onboarding (#393) * button setup * LibraryCategoryView message * refactor * deprecation * ActionCell * onboarding * LibraryLanguageView sort order * preload favicons * refresh when on screen * dependency * schema --- .../xcshareddata/swiftpm/Package.resolved | 8 ++--- .../OPDSRefreshOperation.swift | 18 ++++++++-- Model/Realm/ZimFile.swift | 8 ++--- Model/Services/LibraryService.swift | 8 +++-- Model/Utilities/Enums.swift | 3 +- .../Library/LibraryViewController.swift | 6 ++++ iOS/SwiftUI/BuildingBlocks.swift | 12 +++++-- iOS/SwiftUI/Library/LibraryCategoryView.swift | 36 ++++++++++++------- iOS/SwiftUI/Library/LibraryLanguageView.swift | 14 ++++---- iOS/SwiftUI/Library/LibraryPrimaryView.swift | 27 ++++++++++++++ iOS/SwiftUI/Library/LibrarySettingsView.swift | 2 +- 11 files changed, 104 insertions(+), 38 deletions(-) diff --git a/Kiwix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Kiwix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index de84e7c1..d6eca680 100644 --- a/Kiwix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Kiwix.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "83a07f6a508c3427058d9e2c466208d0b6a960fa", - "version": "10.12.0" + "revision": "e7e7f072a1571435049683ca43c51501de2612fd", + "version": "10.14.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/realm/realm-core", "state": { "branch": null, - "revision": "e72b3078bfc5c3f69a0b18f7a220be27e28c463f", - "version": "11.2.0" + "revision": "fdb2157346dcdf0c2677b3608d9a4c30315fa7f0", + "version": "11.3.1" } }, { diff --git a/Model/Operations/OPDSRefreshOperation/OPDSRefreshOperation.swift b/Model/Operations/OPDSRefreshOperation/OPDSRefreshOperation.swift index 42c633cb..86732329 100644 --- a/Model/Operations/OPDSRefreshOperation/OPDSRefreshOperation.swift +++ b/Model/Operations/OPDSRefreshOperation/OPDSRefreshOperation.swift @@ -28,11 +28,23 @@ class OPDSRefreshOperation: Operation { let parser = OPDSStreamParser() try parser.parse(data: data) try processData(parser: parser) + + // if library has never been refreshed before, preload wikipedia favicons + if Defaults[.libraryLastRefresh] == nil, let languageCode = Locale.current.languageCode { + (try? Realm())?.objects(ZimFile.self) + .filter(NSCompoundPredicate(andPredicateWithSubpredicates: [ + NSPredicate(format: "categoryRaw = %@", ZimFile.Category.wikipedia.rawValue), + NSPredicate(format: "languageCode = %@", languageCode), + NSPredicate(format: "faviconData = nil"), + NSPredicate(format: "faviconURL != nil"), + ])) + .forEach { FaviconDownloadService.shared.download(zimFile: $0) } + } DispatchQueue.main.sync { - // apply language filter if library has never been refreshed - if Defaults[.libraryLastRefresh] == nil, let code = Locale.current.languageCode { - Defaults[.libraryLanguageCodes] = [code] + // if library has never been refreshed before, apply initial language filter + if Defaults[.libraryLastRefresh] == nil, let languageCode = Locale.current.languageCode { + Defaults[.libraryLanguageCodes] = [languageCode] } // update last library refresh time diff --git a/Model/Realm/ZimFile.swift b/Model/Realm/ZimFile.swift index 06fd6582..e779e67f 100644 --- a/Model/Realm/ZimFile.swift +++ b/Model/Realm/ZimFile.swift @@ -24,12 +24,12 @@ class ZimFile: Object, ObjectKeyIdentifiable { @Persisted(indexed: true) var languageCode: String = "" @Persisted(indexed: true) var creationDate: Date = Date() @Persisted(indexed: true) var size: Int64 = 0 - @Persisted var articleCount: Int64 = 0 - @Persisted var mediaCount: Int64 = 0 + @Persisted(indexed: true) var articleCount: Int64 = 0 + @Persisted(indexed: true) var mediaCount: Int64 = 0 + @Persisted(indexed: true) var categoryRaw: String = Category.other.rawValue + @Persisted(indexed: true) var stateRaw: String = State.remote.rawValue @Persisted var creator: String = "" @Persisted var publisher: String = "" - @Persisted var categoryRaw: String = Category.other.rawValue - @Persisted var stateRaw: String = State.remote.rawValue // MARK: - bool properties diff --git a/Model/Services/LibraryService.swift b/Model/Services/LibraryService.swift index 8ab93d8d..65812d3b 100644 --- a/Model/Services/LibraryService.swift +++ b/Model/Services/LibraryService.swift @@ -10,15 +10,12 @@ import os #if canImport(UIKit) import UIKit #endif -import Combine import Defaults import RealmSwift class LibraryService { static let shared = LibraryService() - private var faviconDownloadPipeline: Any? - func isFileInDocumentDirectory(zimFileID: String) -> Bool { if let fileName = ZimFileService.shared.getFileURL(zimFileID: zimFileID)?.lastPathComponent, let documentDirectoryURL = try? FileManager.default.url( @@ -60,6 +57,11 @@ class LibraryService { #if canImport(UIKit) static let autoUpdateInterval: TimeInterval = 3600.0 * 6 + static var isOutdated: Bool { + guard let lastRefresh = Defaults[.libraryLastRefresh] else { return true } + return Date().timeIntervalSince(lastRefresh) > LibraryService.autoUpdateInterval + } + func applyAutoUpdateSetting() { UIApplication.shared.setMinimumBackgroundFetchInterval( Defaults[.libraryAutoRefresh] ? LibraryService.autoUpdateInterval : UIApplication.backgroundFetchIntervalNever diff --git a/Model/Utilities/Enums.swift b/Model/Utilities/Enums.swift index ca95b0c2..f5ff72e9 100644 --- a/Model/Utilities/Enums.swift +++ b/Model/Utilities/Enums.swift @@ -24,9 +24,10 @@ enum ExternalLinkLoadingPolicy: String, CaseIterable, CustomStringConvertible, I } } -enum LibraryLanguageSortingMode: String, Codable, CustomStringConvertible, Defaults.Serializable { +enum LibraryLanguageSortingMode: String, CaseIterable, Codable, CustomStringConvertible, Identifiable, Defaults.Serializable { case alphabetically, byCount + var id: String { self.rawValue } var description: String { switch self { case .alphabetically: diff --git a/iOS/Controller/Library/LibraryViewController.swift b/iOS/Controller/Library/LibraryViewController.swift index e4061596..d0f9dfef 100644 --- a/iOS/Controller/Library/LibraryViewController.swift +++ b/iOS/Controller/Library/LibraryViewController.swift @@ -8,6 +8,7 @@ import SwiftUI import UIKit +import Defaults import RealmSwift @available(iOS 13.0, *) @@ -67,6 +68,11 @@ class LibraryViewController: UISplitViewController, UISplitViewControllerDelegat searchResultsController.rootView.zimFileSelected = { [unowned self] zimFileID, title in self.showZimFile(zimFileID, title) } + + // refresh library when library is opened, but only when library has been previously refreshed + if Defaults[.libraryLastRefresh] != nil, Defaults[.libraryAutoRefresh], LibraryService.isOutdated { + LibraryOperationQueue.shared.addOperation(OPDSRefreshOperation()) + } } // MARK: - Delegates diff --git a/iOS/SwiftUI/BuildingBlocks.swift b/iOS/SwiftUI/BuildingBlocks.swift index c8d9b95a..fd822830 100644 --- a/iOS/SwiftUI/BuildingBlocks.swift +++ b/iOS/SwiftUI/BuildingBlocks.swift @@ -13,22 +13,28 @@ import WebKit struct ActionCell: View { let title: String let isDestructive: Bool + let alignment: HorizontalAlignment let action: (() -> Void) - init(title: String, isDestructive: Bool = false, action: @escaping (() -> Void) = {}) { + init(title: String, + isDestructive: Bool = false, + alignment: HorizontalAlignment = .center, + action: @escaping (() -> Void) = {} + ) { self.title = title self.isDestructive = isDestructive + self.alignment = alignment self.action = action } var body: some View { Button(action: action, label: { HStack { - Spacer() + if alignment != .leading { Spacer() } Text(title) .fontWeight(.medium) .foregroundColor(isDestructive ? .red : nil) - Spacer() + if alignment != .trailing { Spacer() } } }) } diff --git a/iOS/SwiftUI/Library/LibraryCategoryView.swift b/iOS/SwiftUI/Library/LibraryCategoryView.swift index ba826cfd..f1473dc6 100644 --- a/iOS/SwiftUI/Library/LibraryCategoryView.swift +++ b/iOS/SwiftUI/Library/LibraryCategoryView.swift @@ -15,6 +15,7 @@ import RealmSwift @available(iOS 13.0, *) struct LibraryCategoryView: View { @ObservedObject private var viewModel: ViewModel + @Default(.libraryLastRefresh) private var libraryLastRefresh let category: ZimFile.Category var zimFileTapped: (String, String) -> Void = { _, _ in } @@ -25,19 +26,7 @@ struct LibraryCategoryView: View { } var body: some View { - if let languages = viewModel.languages, languages.isEmpty { - InfoView( - imageSystemName: { - if #available(iOS 14.0, *) { - return "text.book.closed" - } else { - return "book" - } - }(), - title: "No Zim Files", - help: "Enable some other languages to see zim files under this category." - ) - } else if let languages = viewModel.languages { + if let languages = viewModel.languages, !languages.isEmpty { List { ForEach(languages) { language in Section(header: languages.count > 1 ? Text(language.name) : nil) { @@ -50,6 +39,27 @@ struct LibraryCategoryView: View { } } } + } else if let languages = viewModel.languages, languages.isEmpty { + InfoView( + imageSystemName: { + if #available(iOS 14.0, *) { + return "text.book.closed" + } else { + return "book" + } + }(), + title: "No Zim Files", + help: { + if libraryLastRefresh == nil { + return "Download online catalog to see zim files under this category." + } else { + return "Enable some other languages to see zim files under this category." + } + }() + ) + } else { + // show nothing when catagory hasn't been fully loaded + EmptyView() } } diff --git a/iOS/SwiftUI/Library/LibraryLanguageView.swift b/iOS/SwiftUI/Library/LibraryLanguageView.swift index bca48eeb..4d3719c7 100644 --- a/iOS/SwiftUI/Library/LibraryLanguageView.swift +++ b/iOS/SwiftUI/Library/LibraryLanguageView.swift @@ -21,17 +21,19 @@ struct LibraryLanguageView: View { list.navigationTitle("Languages").toolbar { ToolbarItem(placement: .navigationBarTrailing) { Picker(selection: $sortingMode, label: Image(systemName: "arrow.up.arrow.down")) { - Text("Alphabetically").tag(LibraryLanguageSortingMode.alphabetically) - Text("By Count").tag(LibraryLanguageSortingMode.byCount) + ForEach(LibraryLanguageSortingMode.allCases) { sortingMode in + Text(sortingMode.description).tag(sortingMode) + } }.pickerStyle(MenuPickerStyle()) } } } else { list.navigationBarItems(trailing: HStack { - Picker("Language Sorting Mode", selection: $sortingMode, content: { - Text("A-Z").tag(LibraryLanguageSortingMode.alphabetically) - Text("By Count").tag(LibraryLanguageSortingMode.byCount) - }).pickerStyle(SegmentedPickerStyle()) + Picker(selection: $sortingMode, label: Image(systemName: "arrow.up.arrow.down")) { + ForEach(LibraryLanguageSortingMode.allCases) { sortingMode in + Text(sortingMode.description).tag(sortingMode) + } + }.pickerStyle(SegmentedPickerStyle()) Spacer(minLength: 60) }) } diff --git a/iOS/SwiftUI/Library/LibraryPrimaryView.swift b/iOS/SwiftUI/Library/LibraryPrimaryView.swift index fe92ebd1..2f5c1d33 100644 --- a/iOS/SwiftUI/Library/LibraryPrimaryView.swift +++ b/iOS/SwiftUI/Library/LibraryPrimaryView.swift @@ -7,11 +7,13 @@ // import SwiftUI +import Defaults import RealmSwift /// A list of all on device & downloading zim files and all zim file categories. @available(iOS 13.0, *) struct LibraryPrimaryView: View { + @Default(.libraryLastRefresh) private var libraryLastRefresh @ObservedResults( ZimFile.self, configuration: Realm.defaultConfig, @@ -27,11 +29,20 @@ struct LibraryPrimaryView: View { ), sortDescriptor: SortDescriptor(keyPath: "size", ascending: false) ) private var download + @ObservedObject private var viewModel = ViewModel() var zimFileSelected: (String, String) -> Void = { _, _ in } var categorySelected: (ZimFile.Category) -> Void = { _ in } var body: some View { List { + if onDevice.count == 0, libraryLastRefresh == nil { + Section(header: Text("Get Started")) { + ActionCell( + title: viewModel.isRefreshing ? "Refreshing..." : "Download Online Catalog", + alignment: .leading + ) { viewModel.refresh() }.disabled(viewModel.isRefreshing) + } + } if onDevice.count > 0 { Section(header: Text("On Device")) { ForEach(onDevice) { zimFile in @@ -72,4 +83,20 @@ struct LibraryPrimaryView: View { } }.listStyle(GroupedListStyle()) } + + class ViewModel: ObservableObject { + @Published private(set) var isRefreshing = false + + init() { + if let operation = LibraryOperationQueue.shared.currentOPDSRefreshOperation { + isRefreshing = !operation.isFinished + } + } + + func refresh() { + guard LibraryOperationQueue.shared.currentOPDSRefreshOperation == nil else { return } + LibraryOperationQueue.shared.addOperation(OPDSRefreshOperation()) + isRefreshing = true + } + } } diff --git a/iOS/SwiftUI/Library/LibrarySettingsView.swift b/iOS/SwiftUI/Library/LibrarySettingsView.swift index 629df9b1..47399e5d 100644 --- a/iOS/SwiftUI/Library/LibrarySettingsView.swift +++ b/iOS/SwiftUI/Library/LibrarySettingsView.swift @@ -59,7 +59,7 @@ struct LibrarySettingsView: View { } private class ViewModel: ObservableObject { - @Published var isRefreshing = false + @Published private(set) var isRefreshing = false private var refreshObserver: NSKeyValueObservation? private let autoRefreshObserver = Defaults.observe(.libraryAutoRefresh) { _ in