mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-15 07:06:21 -04:00
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:
parent
c8cdf00a67
commit
05331031c5
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -29,10 +29,22 @@ class OPDSRefreshOperation: Operation {
|
|||||||
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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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() }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user