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",
"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"
}
},
{

View File

@ -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

View File

@ -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

View File

@ -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

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

View File

@ -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

View File

@ -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() }
}
})
}

View File

@ -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()
}
}

View File

@ -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)
})
}

View File

@ -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
}
}
}

View File

@ -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