Onboarding (#393)

* button setup

* LibraryCategoryView message

* refactor

* deprecation

* ActionCell

* onboarding

* LibraryLanguageView sort order

* preload favicons

* refresh when on screen

* dependency

* schema
This commit is contained in:
ChrisLi 2021-09-06 18:34:02 -04:00 committed by GitHub
parent c8cdf00a67
commit 05331031c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 104 additions and 38 deletions

View File

@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/realm/realm-cocoa", "repositoryURL": "https://github.com/realm/realm-cocoa",
"state": { "state": {
"branch": null, "branch": null,
"revision": "83a07f6a508c3427058d9e2c466208d0b6a960fa", "revision": "e7e7f072a1571435049683ca43c51501de2612fd",
"version": "10.12.0" "version": "10.14.0"
} }
}, },
{ {
@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/realm/realm-core", "repositoryURL": "https://github.com/realm/realm-core",
"state": { "state": {
"branch": null, "branch": null,
"revision": "e72b3078bfc5c3f69a0b18f7a220be27e28c463f", "revision": "fdb2157346dcdf0c2677b3608d9a4c30315fa7f0",
"version": "11.2.0" "version": "11.3.1"
} }
}, },
{ {

View File

@ -28,11 +28,23 @@ class OPDSRefreshOperation: Operation {
let parser = OPDSStreamParser() let parser = OPDSStreamParser()
try parser.parse(data: data) try parser.parse(data: data)
try processData(parser: parser) 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 { DispatchQueue.main.sync {
// apply language filter if library has never been refreshed // if library has never been refreshed before, apply initial language filter
if Defaults[.libraryLastRefresh] == nil, let code = Locale.current.languageCode { if Defaults[.libraryLastRefresh] == nil, let languageCode = Locale.current.languageCode {
Defaults[.libraryLanguageCodes] = [code] Defaults[.libraryLanguageCodes] = [languageCode]
} }
// update last library refresh time // update last library refresh time

View File

@ -24,12 +24,12 @@ class ZimFile: Object, ObjectKeyIdentifiable {
@Persisted(indexed: true) var languageCode: String = "" @Persisted(indexed: true) var languageCode: String = ""
@Persisted(indexed: true) var creationDate: Date = Date() @Persisted(indexed: true) var creationDate: Date = Date()
@Persisted(indexed: true) var size: Int64 = 0 @Persisted(indexed: true) var size: Int64 = 0
@Persisted var articleCount: Int64 = 0 @Persisted(indexed: true) var articleCount: Int64 = 0
@Persisted var mediaCount: 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 creator: String = ""
@Persisted var publisher: String = "" @Persisted var publisher: String = ""
@Persisted var categoryRaw: String = Category.other.rawValue
@Persisted var stateRaw: String = State.remote.rawValue
// MARK: - bool properties // MARK: - bool properties

View File

@ -10,15 +10,12 @@ import os
#if canImport(UIKit) #if canImport(UIKit)
import UIKit import UIKit
#endif #endif
import Combine
import Defaults import Defaults
import RealmSwift import RealmSwift
class LibraryService { class LibraryService {
static let shared = LibraryService() static let shared = LibraryService()
private var faviconDownloadPipeline: Any?
func isFileInDocumentDirectory(zimFileID: String) -> Bool { func isFileInDocumentDirectory(zimFileID: String) -> Bool {
if let fileName = ZimFileService.shared.getFileURL(zimFileID: zimFileID)?.lastPathComponent, if let fileName = ZimFileService.shared.getFileURL(zimFileID: zimFileID)?.lastPathComponent,
let documentDirectoryURL = try? FileManager.default.url( let documentDirectoryURL = try? FileManager.default.url(
@ -60,6 +57,11 @@ class LibraryService {
#if canImport(UIKit) #if canImport(UIKit)
static let autoUpdateInterval: TimeInterval = 3600.0 * 6 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() { func applyAutoUpdateSetting() {
UIApplication.shared.setMinimumBackgroundFetchInterval( UIApplication.shared.setMinimumBackgroundFetchInterval(
Defaults[.libraryAutoRefresh] ? LibraryService.autoUpdateInterval : UIApplication.backgroundFetchIntervalNever Defaults[.libraryAutoRefresh] ? LibraryService.autoUpdateInterval : UIApplication.backgroundFetchIntervalNever

View File

@ -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 case alphabetically, byCount
var id: String { self.rawValue }
var description: String { var description: String {
switch self { switch self {
case .alphabetically: case .alphabetically:

View File

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
import UIKit import UIKit
import Defaults
import RealmSwift import RealmSwift
@available(iOS 13.0, *) @available(iOS 13.0, *)
@ -67,6 +68,11 @@ class LibraryViewController: UISplitViewController, UISplitViewControllerDelegat
searchResultsController.rootView.zimFileSelected = { searchResultsController.rootView.zimFileSelected = {
[unowned self] zimFileID, title in self.showZimFile(zimFileID, title) [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 // MARK: - Delegates

View File

@ -13,22 +13,28 @@ import WebKit
struct ActionCell: View { struct ActionCell: View {
let title: String let title: String
let isDestructive: Bool let isDestructive: Bool
let alignment: HorizontalAlignment
let action: (() -> Void) 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.title = title
self.isDestructive = isDestructive self.isDestructive = isDestructive
self.alignment = alignment
self.action = action self.action = action
} }
var body: some View { var body: some View {
Button(action: action, label: { Button(action: action, label: {
HStack { HStack {
Spacer() if alignment != .leading { Spacer() }
Text(title) Text(title)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(isDestructive ? .red : nil) .foregroundColor(isDestructive ? .red : nil)
Spacer() if alignment != .trailing { Spacer() }
} }
}) })
} }

View File

@ -15,6 +15,7 @@ import RealmSwift
@available(iOS 13.0, *) @available(iOS 13.0, *)
struct LibraryCategoryView: View { struct LibraryCategoryView: View {
@ObservedObject private var viewModel: ViewModel @ObservedObject private var viewModel: ViewModel
@Default(.libraryLastRefresh) private var libraryLastRefresh
let category: ZimFile.Category let category: ZimFile.Category
var zimFileTapped: (String, String) -> Void = { _, _ in } var zimFileTapped: (String, String) -> Void = { _, _ in }
@ -25,19 +26,7 @@ struct LibraryCategoryView: View {
} }
var body: some View { var body: some View {
if let languages = viewModel.languages, languages.isEmpty { 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 {
List { List {
ForEach(languages) { language in ForEach(languages) { language in
Section(header: languages.count > 1 ? Text(language.name) : nil) { 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()
} }
} }

View File

@ -21,17 +21,19 @@ struct LibraryLanguageView: View {
list.navigationTitle("Languages").toolbar { list.navigationTitle("Languages").toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Picker(selection: $sortingMode, label: Image(systemName: "arrow.up.arrow.down")) { Picker(selection: $sortingMode, label: Image(systemName: "arrow.up.arrow.down")) {
Text("Alphabetically").tag(LibraryLanguageSortingMode.alphabetically) ForEach(LibraryLanguageSortingMode.allCases) { sortingMode in
Text("By Count").tag(LibraryLanguageSortingMode.byCount) Text(sortingMode.description).tag(sortingMode)
}
}.pickerStyle(MenuPickerStyle()) }.pickerStyle(MenuPickerStyle())
} }
} }
} else { } else {
list.navigationBarItems(trailing: HStack { list.navigationBarItems(trailing: HStack {
Picker("Language Sorting Mode", selection: $sortingMode, content: { Picker(selection: $sortingMode, label: Image(systemName: "arrow.up.arrow.down")) {
Text("A-Z").tag(LibraryLanguageSortingMode.alphabetically) ForEach(LibraryLanguageSortingMode.allCases) { sortingMode in
Text("By Count").tag(LibraryLanguageSortingMode.byCount) Text(sortingMode.description).tag(sortingMode)
}).pickerStyle(SegmentedPickerStyle()) }
}.pickerStyle(SegmentedPickerStyle())
Spacer(minLength: 60) Spacer(minLength: 60)
}) })
} }

View File

@ -7,11 +7,13 @@
// //
import SwiftUI import SwiftUI
import Defaults
import RealmSwift import RealmSwift
/// A list of all on device & downloading zim files and all zim file categories. /// A list of all on device & downloading zim files and all zim file categories.
@available(iOS 13.0, *) @available(iOS 13.0, *)
struct LibraryPrimaryView: View { struct LibraryPrimaryView: View {
@Default(.libraryLastRefresh) private var libraryLastRefresh
@ObservedResults( @ObservedResults(
ZimFile.self, ZimFile.self,
configuration: Realm.defaultConfig, configuration: Realm.defaultConfig,
@ -27,11 +29,20 @@ struct LibraryPrimaryView: View {
), ),
sortDescriptor: SortDescriptor(keyPath: "size", ascending: false) sortDescriptor: SortDescriptor(keyPath: "size", ascending: false)
) private var download ) private var download
@ObservedObject private var viewModel = ViewModel()
var zimFileSelected: (String, String) -> Void = { _, _ in } var zimFileSelected: (String, String) -> Void = { _, _ in }
var categorySelected: (ZimFile.Category) -> Void = { _ in } var categorySelected: (ZimFile.Category) -> Void = { _ in }
var body: some View { var body: some View {
List { 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 { if onDevice.count > 0 {
Section(header: Text("On Device")) { Section(header: Text("On Device")) {
ForEach(onDevice) { zimFile in ForEach(onDevice) { zimFile in
@ -72,4 +83,20 @@ struct LibraryPrimaryView: View {
} }
}.listStyle(GroupedListStyle()) }.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
}
}
} }

View File

@ -59,7 +59,7 @@ struct LibrarySettingsView: View {
} }
private class ViewModel: ObservableObject { private class ViewModel: ObservableObject {
@Published var isRefreshing = false @Published private(set) var isRefreshing = false
private var refreshObserver: NSKeyValueObservation? private var refreshObserver: NSKeyValueObservation?
private let autoRefreshObserver = Defaults.observe(.libraryAutoRefresh) { _ in private let autoRefreshObserver = Defaults.observe(.libraryAutoRefresh) { _ in