mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-22 11:03:21 -04:00
Merge pull request #1008 from kiwix/1006-improve-launch-sequence-for-custom-apps
Improve launch sequence for custom apps
This commit is contained in:
commit
c78d229dac
@ -127,7 +127,7 @@ struct RootView: View {
|
||||
} detail: {
|
||||
switch navigation.currentItem {
|
||||
case .loading:
|
||||
LoadingView()
|
||||
LoadingDataView()
|
||||
case .reading:
|
||||
BrowserTab().environmentObject(browser)
|
||||
.withHostingWindow { window in
|
||||
|
@ -22,6 +22,7 @@ import Combine
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import CoreData
|
||||
import Defaults
|
||||
|
||||
final class CompactViewController: UIHostingController<AnyView>, UISearchControllerDelegate, UISearchResultsUpdating {
|
||||
private let searchViewModel: SearchViewModel
|
||||
@ -133,6 +134,7 @@ final class CompactViewController: UIHostingController<AnyView>, UISearchControl
|
||||
|
||||
private struct CompactView: View {
|
||||
@EnvironmentObject private var navigation: NavigationViewModel
|
||||
@EnvironmentObject private var library: LibraryViewModel
|
||||
@State private var presentedSheet: PresentedSheet?
|
||||
|
||||
private enum PresentedSheet: String, Identifiable {
|
||||
@ -149,8 +151,14 @@ private struct CompactView: View {
|
||||
|
||||
var body: some View {
|
||||
if case .loading = navigation.currentItem {
|
||||
LoadingView()
|
||||
LoadingDataView()
|
||||
} else if case let .tab(tabID) = navigation.currentItem {
|
||||
let browser = BrowserViewModel.getCached(tabID: tabID)
|
||||
let model = if FeatureFlags.hasLibrary {
|
||||
CatalogLaunchViewModel(library: library, browser: browser)
|
||||
} else {
|
||||
NoCatalogLaunchViewModel(browser: browser)
|
||||
}
|
||||
Content(tabID: tabID, showLibrary: {
|
||||
if presentedSheet == nil {
|
||||
presentedSheet = .library
|
||||
@ -158,7 +166,7 @@ private struct CompactView: View {
|
||||
// there's a sheet already presented by the user
|
||||
// do nothing
|
||||
}
|
||||
})
|
||||
}, model: model)
|
||||
.id(tabID)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
@ -189,7 +197,7 @@ private struct CompactView: View {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.environmentObject(BrowserViewModel.getCached(tabID: tabID))
|
||||
.environmentObject(browser)
|
||||
.sheet(item: $presentedSheet) { presentedSheet in
|
||||
switch presentedSheet {
|
||||
case .library:
|
||||
@ -212,22 +220,49 @@ private struct CompactView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct Content: View {
|
||||
private struct Content<LaunchModel>: View where LaunchModel: LaunchProtocol {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@EnvironmentObject private var browser: BrowserViewModel
|
||||
@EnvironmentObject private var library: LibraryViewModel
|
||||
@EnvironmentObject private var navigation: NavigationViewModel
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)],
|
||||
predicate: ZimFile.openedPredicate
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
|
||||
/// this is still hacky a bit, as the change from here re-validates the view
|
||||
/// which triggers the model to be revalidated
|
||||
@Default(.hasSeenCategories) private var hasSeenCategories
|
||||
let tabID: NSManagedObjectID?
|
||||
let showLibrary: () -> Void
|
||||
@ObservedObject var model: LaunchModel
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if browser.url == nil {
|
||||
Welcome(showLibrary: showLibrary)
|
||||
} else {
|
||||
WebView().ignoresSafeArea()
|
||||
// swiftlint:disable:next redundant_discardable_let
|
||||
let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty,
|
||||
hasSeenCategories: hasSeenCategories)
|
||||
switch model.state {
|
||||
case .loadingData:
|
||||
LoadingDataView()
|
||||
case .webPage(let isLoading):
|
||||
WebView()
|
||||
.ignoresSafeArea()
|
||||
.overlay {
|
||||
if isLoading {
|
||||
LoadingProgressView()
|
||||
}
|
||||
}
|
||||
case .catalog(let catalogSequence):
|
||||
switch catalogSequence {
|
||||
case .fetching:
|
||||
FetchingCatalogView()
|
||||
case .list:
|
||||
LocalLibraryList()
|
||||
case .welcome(let welcomeViewState):
|
||||
WelcomeCatalog(viewState: welcomeViewState)
|
||||
}
|
||||
}
|
||||
}
|
||||
.focusedSceneValue(\.browserViewModel, browser)
|
||||
@ -262,6 +297,23 @@ private struct Content: View {
|
||||
browser.refreshVideoState()
|
||||
}
|
||||
}
|
||||
.onChange(of: library.state) { state in
|
||||
guard state == .complete else { return }
|
||||
showTheLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
private func showTheLibrary() {
|
||||
guard model.state.shouldShowCatalog else { return }
|
||||
#if os(macOS)
|
||||
navigation.currentItem = .categories
|
||||
#else
|
||||
if horizontalSizeClass == .regular {
|
||||
navigation.currentItem = .categories
|
||||
} else {
|
||||
showLibrary()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -141,7 +141,7 @@ final class SplitViewController: UISplitViewController {
|
||||
let controller = UIHostingController(rootView: Settings())
|
||||
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
|
||||
case .loading:
|
||||
let controller = UIHostingController(rootView: LoadingView())
|
||||
let controller = UIHostingController(rootView: LoadingDataView())
|
||||
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
|
||||
default:
|
||||
let controller = UIHostingController(rootView: Text("vc-not-implemented"))
|
||||
|
@ -51,6 +51,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@Published private(set) var isLoading: Bool?
|
||||
@Published private(set) var canGoBack = false
|
||||
@Published private(set) var canGoForward = false
|
||||
@Published private(set) var articleTitle: String = ""
|
||||
@ -58,6 +59,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
@Published private(set) var articleBookmarked = false
|
||||
@Published private(set) var outlineItems = [OutlineItem]()
|
||||
@Published private(set) var outlineItemTree = [OutlineItem]()
|
||||
@MainActor @Published private(set) var hasURL: Bool = false
|
||||
@MainActor @Published private(set) var url: URL? {
|
||||
didSet {
|
||||
if !FeatureFlags.hasLibrary, url == nil {
|
||||
@ -67,6 +69,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
bookmarkFetchedResultsController.fetchRequest.predicate = Self.bookmarksPredicateFor(url: url)
|
||||
try? bookmarkFetchedResultsController.performFetch()
|
||||
}
|
||||
hasURL = url != nil
|
||||
}
|
||||
}
|
||||
@Published var externalURL: URL?
|
||||
@ -87,6 +90,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
}
|
||||
#endif
|
||||
let webView: WKWebView
|
||||
private var isLoadingObserver: NSKeyValueObservation?
|
||||
private var canGoBackObserver: NSKeyValueObservation?
|
||||
private var canGoForwardObserver: NSKeyValueObservation?
|
||||
private var titleURLObserver: AnyCancellable?
|
||||
@ -96,8 +100,8 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
@MainActor
|
||||
init(tabID: NSManagedObjectID? = nil) {
|
||||
// swiftlint:disable:next function_body_length
|
||||
@MainActor init(tabID: NSManagedObjectID? = nil) {
|
||||
self.tabID = tabID
|
||||
webView = WKWebView(frame: .zero, configuration: WebViewConfiguration())
|
||||
if !Bundle.main.isProduction, #available(iOS 16.4, macOS 13.3, *) {
|
||||
@ -159,6 +163,14 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
||||
guard let title, let url else { return }
|
||||
self?.didUpdate(title: title, url: url)
|
||||
}
|
||||
|
||||
isLoadingObserver = webView.observe(\.isLoading, options: .new) { [weak self] _, change in
|
||||
Task { @MainActor [weak self] in
|
||||
if change.newValue != self?.isLoading {
|
||||
self?.isLoading = change.newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the webpage in a binary format
|
||||
|
172
ViewModel/LaunchViewModel.swift
Normal file
172
ViewModel/LaunchViewModel.swift
Normal file
@ -0,0 +1,172 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Defaults
|
||||
|
||||
enum LaunchSequence: Equatable {
|
||||
case loadingData
|
||||
case webPage(isLoading: Bool)
|
||||
case catalog(CatalogSequence)
|
||||
|
||||
var shouldShowCatalog: Bool {
|
||||
switch self {
|
||||
case .loadingData: return true
|
||||
case .webPage: return false
|
||||
case .catalog(.fetching): return true
|
||||
case .catalog(.welcome): return true
|
||||
case .catalog(.list): return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CatalogSequence: Equatable {
|
||||
case fetching
|
||||
case list
|
||||
case welcome(WelcomeViewState)
|
||||
}
|
||||
|
||||
enum WelcomeViewState: Equatable {
|
||||
case loading
|
||||
case error
|
||||
case complete
|
||||
}
|
||||
|
||||
protocol LaunchProtocol: ObservableObject {
|
||||
var state: LaunchSequence { get }
|
||||
func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool)
|
||||
}
|
||||
|
||||
// MARK: No Library (custom apps)
|
||||
|
||||
/// Keeps us int the .loadingData state,
|
||||
/// while the main page is not fully loaded for the first time
|
||||
final class NoCatalogLaunchViewModel: LaunchViewModelBase {
|
||||
|
||||
private static var wasLoaded = false
|
||||
|
||||
convenience init(browser: BrowserViewModel) {
|
||||
self.init(browserIsLoading: browser.$isLoading)
|
||||
}
|
||||
|
||||
/// - Parameter browserIsLoading: assumed to start with a nil value (see: WKWebView.isLoading)
|
||||
init(browserIsLoading: Published<Bool?>.Publisher) {
|
||||
super.init()
|
||||
browserIsLoading.sink { [weak self] (isLoading: Bool?) in
|
||||
guard let self = self else { return }
|
||||
switch isLoading {
|
||||
case .none:
|
||||
updateTo(.loadingData)
|
||||
case .some(true):
|
||||
if !Self.wasLoaded {
|
||||
updateTo(.loadingData)
|
||||
} else {
|
||||
updateTo(.webPage(isLoading: true))
|
||||
}
|
||||
case .some(false):
|
||||
Self.wasLoaded = true
|
||||
updateTo(.webPage(isLoading: false))
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) {
|
||||
// to be ignored on purpose
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: With Catalog Library
|
||||
final class CatalogLaunchViewModel: LaunchViewModelBase {
|
||||
|
||||
private var hasZIMFiles = CurrentValueSubject<Bool, Never>(false)
|
||||
private var hasSeenCategoriesOnce = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
convenience init(library: LibraryViewModel,
|
||||
browser: BrowserViewModel) {
|
||||
self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading)
|
||||
}
|
||||
|
||||
// swiftlint:disable closure_parameter_position
|
||||
init(libraryState: Published<LibraryState>.Publisher,
|
||||
browserIsLoading: Published<Bool?>.Publisher) {
|
||||
super.init()
|
||||
|
||||
hasZIMFiles.combineLatest(
|
||||
libraryState,
|
||||
browserIsLoading,
|
||||
hasSeenCategoriesOnce
|
||||
).sink { [weak self] (
|
||||
hasZIMs: Bool,
|
||||
libState: LibraryState,
|
||||
isBrowserLoading: Bool?,
|
||||
hasSeenCategories: Bool
|
||||
) in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch (isBrowserLoading, hasZIMs, libState) {
|
||||
|
||||
// MARK: initial app start state
|
||||
case (_, _, .initial): updateTo(.loadingData)
|
||||
|
||||
// MARK: browser must be empty as there are no ZIMs:
|
||||
case (_, false, .inProgress):
|
||||
if hasSeenCategories {
|
||||
updateTo(.catalog(.welcome(.loading)))
|
||||
} else {
|
||||
updateTo(.catalog(.fetching))
|
||||
}
|
||||
case (_, false, .complete):
|
||||
if hasSeenCategories {
|
||||
updateTo(.catalog(.welcome(.complete)))
|
||||
} else {
|
||||
updateTo(.catalog(.fetching))
|
||||
}
|
||||
case (_, false, .error):
|
||||
// safety path to display the welcome buttons
|
||||
// in case of a fetch error, the user can try again
|
||||
hasSeenCategoriesOnce.send(true)
|
||||
updateTo(.catalog(.welcome(.error)))
|
||||
|
||||
// MARK: has zims and opens a new empty tab
|
||||
case (.none, true, _): updateTo(.catalog(.list))
|
||||
|
||||
// MARK: actively browsing
|
||||
case (.some(true), true, _): updateTo(.webPage(isLoading: true))
|
||||
case (.some(false), true, _): updateTo(.webPage(isLoading: false))
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
// swiftlint:enable closure_parameter_position
|
||||
|
||||
override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) {
|
||||
hasZIMFiles.send(hasZimFiles)
|
||||
hasSeenCategoriesOnce.send(hasSeenCategories)
|
||||
}
|
||||
}
|
||||
|
||||
class LaunchViewModelBase: LaunchProtocol, ObservableObject {
|
||||
var state: LaunchSequence = .loadingData
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
func updateTo(_ newState: LaunchSequence) {
|
||||
guard newState != state else { return }
|
||||
state = newState
|
||||
}
|
||||
|
||||
func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) {
|
||||
fatalError("should be overriden")
|
||||
}
|
||||
}
|
@ -14,18 +14,25 @@
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
/// This is macOS and iPad only specific, not used on iPhone
|
||||
struct BrowserTab: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@EnvironmentObject private var browser: BrowserViewModel
|
||||
@EnvironmentObject private var library: LibraryViewModel
|
||||
@StateObject private var search = SearchViewModel()
|
||||
|
||||
var body: some View {
|
||||
Content().toolbar {
|
||||
#if os(macOS)
|
||||
let model = if FeatureFlags.hasLibrary {
|
||||
CatalogLaunchViewModel(library: library, browser: browser)
|
||||
} else {
|
||||
NoCatalogLaunchViewModel(browser: browser)
|
||||
}
|
||||
Content(model: model).toolbar {
|
||||
#if os(macOS)
|
||||
ToolbarItemGroup(placement: .navigation) { NavigationButtons() }
|
||||
#elseif os(iOS)
|
||||
#elseif os(iOS)
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
if #unavailable(iOS 16) {
|
||||
Button {
|
||||
@ -36,17 +43,17 @@ struct BrowserTab: View {
|
||||
}
|
||||
NavigationButtons()
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
OutlineButton()
|
||||
ExportButton()
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
PrintButton()
|
||||
#endif
|
||||
#endif
|
||||
BookmarkButton()
|
||||
#if os(iOS)
|
||||
#if os(iOS)
|
||||
ContentSearchButton()
|
||||
#endif
|
||||
#endif
|
||||
ArticleShortcutButtons(displayMode: .mainAndRandomArticle)
|
||||
}
|
||||
}
|
||||
@ -62,12 +69,12 @@ struct BrowserTab: View {
|
||||
}
|
||||
}
|
||||
.modify { view in
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
view.navigationTitle(browser.articleTitle.isEmpty ? Brand.appName : browser.articleTitle)
|
||||
.navigationSubtitle(browser.zimFileName)
|
||||
#elseif os(iOS)
|
||||
#elseif os(iOS)
|
||||
view
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
browser.updateLastOpened()
|
||||
@ -78,11 +85,25 @@ struct BrowserTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct Content: View {
|
||||
private struct Content<LaunchModel>: View where LaunchModel: LaunchProtocol {
|
||||
@Environment(\.isSearching) private var isSearching
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@EnvironmentObject private var browser: BrowserViewModel
|
||||
@EnvironmentObject private var library: LibraryViewModel
|
||||
@EnvironmentObject private var navigation: NavigationViewModel
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)],
|
||||
predicate: ZimFile.openedPredicate
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
/// this is still hacky a bit, as the change from here re-validates the view
|
||||
/// which triggers the model to be revalidated
|
||||
@Default(.hasSeenCategories) private var hasSeenCategories
|
||||
@ObservedObject var model: LaunchModel
|
||||
|
||||
var body: some View {
|
||||
// swiftlint:disable:next redundant_discardable_let
|
||||
let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty,
|
||||
hasSeenCategories: hasSeenCategories)
|
||||
GeometryReader { proxy in
|
||||
Group {
|
||||
if isSearching {
|
||||
@ -92,20 +113,46 @@ struct BrowserTab: View {
|
||||
#elseif os(iOS)
|
||||
.environment(\.horizontalSizeClass, proxy.size.width > 750 ? .regular : .compact)
|
||||
#endif
|
||||
} else if browser.url == nil && FeatureFlags.hasLibrary {
|
||||
Welcome(showLibrary: nil)
|
||||
} else {
|
||||
WebView().ignoresSafeArea()
|
||||
#if os(macOS)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
ContentSearchBar(
|
||||
model: ContentSearchViewModel(findInWebPage: browser.webView.find(_:configuration:))
|
||||
)
|
||||
}
|
||||
#endif
|
||||
switch model.state {
|
||||
case .loadingData:
|
||||
LoadingDataView()
|
||||
case .webPage(let isLoading):
|
||||
WebView()
|
||||
.ignoresSafeArea()
|
||||
.overlay {
|
||||
if isLoading {
|
||||
LoadingProgressView()
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
ContentSearchBar(
|
||||
model: ContentSearchViewModel(
|
||||
findInWebPage: browser.webView.find(_:configuration:)
|
||||
)
|
||||
)
|
||||
}
|
||||
#endif
|
||||
case .catalog(.fetching):
|
||||
FetchingCatalogView()
|
||||
case .catalog(.list):
|
||||
LocalLibraryList()
|
||||
case .catalog(.welcome(let welcomeViewState)):
|
||||
WelcomeCatalog(viewState: welcomeViewState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: library.state) { state in
|
||||
guard state == .complete else { return }
|
||||
showTheLibrary()
|
||||
}
|
||||
}
|
||||
|
||||
private func showTheLibrary() {
|
||||
guard model.state.shouldShowCatalog else { return }
|
||||
navigation.currentItem = .categories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +189,16 @@ struct LoadingProgressView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadingView: View {
|
||||
struct FetchingCatalogView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
LogoView()
|
||||
LoadingMessageView(message: "welcome.button.status.fetching_catalog.text".localized)
|
||||
}.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadingDataView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
LogoView()
|
||||
@ -199,5 +208,5 @@ struct LoadingView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LoadingView()
|
||||
LoadingDataView()
|
||||
}
|
||||
|
68
Views/LocalLibraryList.swift
Normal file
68
Views/LocalLibraryList.swift
Normal file
@ -0,0 +1,68 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Defaults
|
||||
|
||||
/// Displays a grid of available local ZIM files. Used on new tab.
|
||||
struct LocalLibraryList: View {
|
||||
@EnvironmentObject private var browser: BrowserViewModel
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.created, ascending: false)],
|
||||
animation: .easeInOut
|
||||
) private var bookmarks: FetchedResults<Bookmark>
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)],
|
||||
predicate: ZimFile.openedPredicate,
|
||||
animation: .easeInOut
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(
|
||||
columns: ([GridItem(.adaptive(minimum: 250, maximum: 500), spacing: 12)]),
|
||||
alignment: .leading,
|
||||
spacing: 12
|
||||
) {
|
||||
GridSection(title: "welcome.main_page.title".localized) {
|
||||
ForEach(zimFiles) { zimFile in
|
||||
AsyncButtonView {
|
||||
guard let url = await ZimFileService.shared
|
||||
.getMainPageURL(zimFileID: zimFile.fileID) else { return }
|
||||
browser.load(url: url)
|
||||
} label: {
|
||||
ZimFileCell(zimFile, prominent: .name)
|
||||
} loading: {
|
||||
ZimFileCell(zimFile, prominent: .name, isLoading: true)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
if !bookmarks.isEmpty {
|
||||
GridSection(title: "welcome.grid.bookmarks.title".localized) {
|
||||
ForEach(bookmarks.prefix(6)) { bookmark in
|
||||
Button {
|
||||
browser.load(url: bookmark.articleURL)
|
||||
} label: {
|
||||
ArticleCell(bookmark: bookmark)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.modifier(BookmarkContextMenu(bookmark: bookmark))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.modifier(GridCommon(edges: .all))
|
||||
}
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Defaults
|
||||
|
||||
struct Welcome: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@EnvironmentObject private var browser: BrowserViewModel
|
||||
@EnvironmentObject private var library: LibraryViewModel
|
||||
@EnvironmentObject private var navigation: NavigationViewModel
|
||||
@Default(.hasSeenCategories) private var hasSeenCategories
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.created, ascending: false)],
|
||||
animation: .easeInOut
|
||||
) private var bookmarks: FetchedResults<Bookmark>
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)],
|
||||
predicate: ZimFile.openedPredicate,
|
||||
animation: .easeInOut
|
||||
) private var zimFiles: FetchedResults<ZimFile>
|
||||
/// Used only for iPhone
|
||||
let showLibrary: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
if zimFiles.isEmpty {
|
||||
ZStack {
|
||||
LogoView()
|
||||
welcomeContent
|
||||
.onAppear {
|
||||
if !hasSeenCategories, library.state == .complete {
|
||||
// safety path for upgrading user with no ZIM files, but fetched categories
|
||||
// to make sure we do display the buttons
|
||||
hasSeenCategories = true
|
||||
}
|
||||
}
|
||||
if library.state == .inProgress {
|
||||
if hasSeenCategories {
|
||||
LoadingProgressView()
|
||||
} else {
|
||||
LoadingMessageView(message: "welcome.button.status.fetching_catalog.text".localized)
|
||||
}
|
||||
}
|
||||
}.ignoresSafeArea()
|
||||
} else {
|
||||
LazyVGrid(
|
||||
columns: ([GridItem(.adaptive(minimum: 250, maximum: 500), spacing: 12)]),
|
||||
alignment: .leading,
|
||||
spacing: 12
|
||||
) {
|
||||
GridSection(title: "welcome.main_page.title".localized) {
|
||||
ForEach(zimFiles) { zimFile in
|
||||
AsyncButtonView {
|
||||
guard let url = await ZimFileService.shared
|
||||
.getMainPageURL(zimFileID: zimFile.fileID) else { return }
|
||||
browser.load(url: url)
|
||||
} label: {
|
||||
ZimFileCell(zimFile, prominent: .name)
|
||||
} loading: {
|
||||
ZimFileCell(zimFile, prominent: .name, isLoading: true)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
if !bookmarks.isEmpty {
|
||||
GridSection(title: "welcome.grid.bookmarks.title".localized) {
|
||||
ForEach(bookmarks.prefix(6)) { bookmark in
|
||||
Button {
|
||||
browser.load(url: bookmark.articleURL)
|
||||
} label: {
|
||||
ArticleCell(bookmark: bookmark)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.modifier(BookmarkContextMenu(bookmark: bookmark))
|
||||
}
|
||||
}
|
||||
}
|
||||
}.modifier(GridCommon(edges: .all))
|
||||
}
|
||||
}
|
||||
|
||||
private var welcomeContent: some View {
|
||||
GeometryReader { geometry in
|
||||
let logoCalc = LogoCalc(
|
||||
geometry: geometry.size,
|
||||
originalImageSize: Brand.loadingLogoSize,
|
||||
horizontal: horizontalSizeClass,
|
||||
vertical: verticalSizeClass
|
||||
)
|
||||
actions
|
||||
.position(
|
||||
x: geometry.size.width * 0.5,
|
||||
y: logoCalc.buttonCenterY)
|
||||
.opacity(hasSeenCategories ? 1 : 0)
|
||||
.frame(maxWidth: logoCalc.buttonsWidth)
|
||||
.onChange(of: library.state) { state in
|
||||
if state == .error {
|
||||
hasSeenCategories = true
|
||||
}
|
||||
guard state == .complete else { return }
|
||||
#if os(macOS)
|
||||
navigation.currentItem = .categories
|
||||
#elseif os(iOS)
|
||||
if horizontalSizeClass == .regular {
|
||||
navigation.currentItem = .categories
|
||||
} else {
|
||||
showLibrary?()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Text("library_refresh_error.retrieve.description".localized)
|
||||
.foregroundColor(.red)
|
||||
.opacity(library.state == .error ? 1 : 0)
|
||||
.position(
|
||||
x: geometry.size.width * 0.5,
|
||||
y: logoCalc.errorTextCenterY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Onboarding actions, open a zim file or refresh catalog
|
||||
private var actions: some View {
|
||||
if verticalSizeClass == .compact { // iPhone landscape
|
||||
AnyView(HStack {
|
||||
openFileButton
|
||||
catalogButton
|
||||
})
|
||||
} else {
|
||||
AnyView(VStack {
|
||||
openFileButton
|
||||
catalogButton
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private var openFileButton: some View {
|
||||
OpenFileButton(context: .onBoarding) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("welcome.actions.open_file".localized)
|
||||
Spacer()
|
||||
}.padding(6)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
private var catalogButton: some View {
|
||||
Button {
|
||||
library.start(isUserInitiated: true)
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
if library.state == .inProgress {
|
||||
Text("welcome.button.status.fetching_catalog.text".localized)
|
||||
} else {
|
||||
Text("welcome.button.status.fetch_catalog.text".localized)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(6)
|
||||
}
|
||||
.disabled(library.state == .inProgress)
|
||||
.font(.subheadline)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Welcome(showLibrary: nil).environmentObject(LibraryViewModel()).preferredColorScheme(.light).padding()
|
||||
Welcome(showLibrary: nil).environmentObject(LibraryViewModel()).preferredColorScheme(.dark).padding()
|
||||
}
|
||||
}
|
111
Views/WelcomeCatalog.swift
Normal file
111
Views/WelcomeCatalog.swift
Normal file
@ -0,0 +1,111 @@
|
||||
// This file is part of Kiwix for iOS & macOS.
|
||||
//
|
||||
// Kiwix is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kiwix is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import Defaults
|
||||
|
||||
/// Displays the Logo and 2 buttons open file | fetch catalog.
|
||||
/// Used on new tab, when no ZIM files are available
|
||||
struct WelcomeCatalog: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@EnvironmentObject private var library: LibraryViewModel
|
||||
let viewState: WelcomeViewState
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
LogoView()
|
||||
welcomeContent
|
||||
}.ignoresSafeArea()
|
||||
}
|
||||
|
||||
private var welcomeContent: some View {
|
||||
GeometryReader { geometry in
|
||||
let logoCalc = LogoCalc(
|
||||
geometry: geometry.size,
|
||||
originalImageSize: Brand.loadingLogoSize,
|
||||
horizontal: horizontalSizeClass,
|
||||
vertical: verticalSizeClass
|
||||
)
|
||||
actions
|
||||
.position(
|
||||
x: geometry.size.width * 0.5,
|
||||
y: logoCalc.buttonCenterY)
|
||||
.frame(maxWidth: logoCalc.buttonsWidth)
|
||||
if viewState == .error {
|
||||
Text("library_refresh_error.retrieve.description".localized)
|
||||
.foregroundColor(.red)
|
||||
.position(
|
||||
x: geometry.size.width * 0.5,
|
||||
y: logoCalc.errorTextCenterY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Onboarding actions, open a zim file or refetch catalog
|
||||
private var actions: some View {
|
||||
if verticalSizeClass == .compact { // iPhone landscape
|
||||
AnyView(HStack {
|
||||
openFileButton
|
||||
fetchCatalogButton
|
||||
})
|
||||
} else {
|
||||
AnyView(VStack {
|
||||
openFileButton
|
||||
fetchCatalogButton
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private var openFileButton: some View {
|
||||
OpenFileButton(context: .onBoarding) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("welcome.actions.open_file".localized)
|
||||
Spacer()
|
||||
}.padding(6)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
private var fetchCatalogButton: some View {
|
||||
Button {
|
||||
library.start(isUserInitiated: true)
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
if viewState == .loading {
|
||||
Text("welcome.button.status.fetching_catalog.text".localized)
|
||||
} else {
|
||||
Text("welcome.button.status.fetch_catalog.text".localized)
|
||||
}
|
||||
Spacer()
|
||||
}.padding(6)
|
||||
}
|
||||
.disabled(viewState == .loading)
|
||||
.font(.subheadline)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WelcomeCatalog(viewState: .loading).environmentObject(LibraryViewModel()).preferredColorScheme(.light).padding()
|
||||
WelcomeCatalog(viewState: .error).environmentObject(LibraryViewModel()).preferredColorScheme(.dark).padding()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user