From 97143cb05b4f4cc3be00e7db3fad697385645117 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 12 Oct 2024 13:33:50 +0200 Subject: [PATCH 01/18] Do not display the grid of ZIMs for custom apps --- Views/Welcome.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Views/Welcome.swift b/Views/Welcome.swift index c7702322..a0f2145a 100644 --- a/Views/Welcome.swift +++ b/Views/Welcome.swift @@ -37,7 +37,7 @@ struct Welcome: View { let showLibrary: (() -> Void)? var body: some View { - if zimFiles.isEmpty { + if zimFiles.isEmpty || !FeatureFlags.hasLibrary { ZStack { LogoView() welcomeContent @@ -56,7 +56,7 @@ struct Welcome: View { } } }.ignoresSafeArea() - } else { + } else if FeatureFlags.hasLibrary { LazyVGrid( columns: ([GridItem(.adaptive(minimum: 250, maximum: 500), spacing: 12)]), alignment: .leading, From 1309d527d59cc781e50e549355b9f519be24fccb Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 12 Oct 2024 22:39:54 +0200 Subject: [PATCH 02/18] Custom apps logic --- App/CompactViewController.swift | 13 ++++++++++++- ViewModel/BrowserViewModel.swift | 10 ++++++++++ Views/Welcome.swift | 30 +++++++++++++++++------------- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 9e573067..ffce8882 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -219,15 +219,21 @@ private struct Content: View { sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], predicate: ZimFile.openedPredicate ) private var zimFiles: FetchedResults + @State var isInitialLoad: Bool = true let tabID: NSManagedObjectID? let showLibrary: () -> Void var body: some View { Group { - if browser.url == nil { + if browser.url == nil || (!FeatureFlags.hasLibrary && isInitialLoad) { Welcome(showLibrary: showLibrary) } else { WebView().ignoresSafeArea() + .overlay { + if browser.isLoading == true { + LoadingProgressView() + } + } } } .focusedSceneValue(\.browserViewModel, browser) @@ -262,6 +268,11 @@ private struct Content: View { browser.refreshVideoState() } } + .onChange(of: browser.isLoading) { isLoading in + if isLoading == false { // wait for the first full webpage load + isInitialLoad = false + } + } } } #endif diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 0c082557..646b333c 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -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 = "" @@ -87,6 +88,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? @@ -159,6 +161,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] webView, change in + Task { @MainActor in + if change.newValue != self?.isLoading { + self?.isLoading = change.newValue + } + } + } } /// Get the webpage in a binary format diff --git a/Views/Welcome.swift b/Views/Welcome.swift index a0f2145a..b9162934 100644 --- a/Views/Welcome.swift +++ b/Views/Welcome.swift @@ -39,20 +39,24 @@ struct Welcome: View { var body: some View { if zimFiles.isEmpty || !FeatureFlags.hasLibrary { 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 !FeatureFlags.hasLibrary && browser.isLoading != false { + LoadingView() + } else { + 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) } - } - if library.state == .inProgress { - if hasSeenCategories { - LoadingProgressView() - } else { - LoadingMessageView(message: "welcome.button.status.fetching_catalog.text".localized) } } }.ignoresSafeArea() From e4f65d83cdf7ce658150436d1cf47d1d53523e4e Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 13 Oct 2024 20:18:30 +0200 Subject: [PATCH 03/18] Add LaunchViewModel --- ViewModel/BrowserViewModel.swift | 2 + ViewModel/LaunchViewModel.swift | 136 +++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 ViewModel/LaunchViewModel.swift diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 646b333c..84e615ca 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -59,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 { @@ -68,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? diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift new file mode 100644 index 00000000..440ed0cc --- /dev/null +++ b/ViewModel/LaunchViewModel.swift @@ -0,0 +1,136 @@ +// 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) +} + +enum CatalogSequence: Equatable { + case fetching + case error + case list + case welcome(isCatalogLoading: Bool) +} + +protocol LaunchViewModelProtocol { + var state: Published.Publisher { get } +} + +// MARK: No Library (custom apps) + +/// Keeps us int the .loadingData state, +/// while the main page is not fully loaded for the first time +@MainActor +final class NoCatalogLaunchViewModel: LaunchViewModelBase { + + private 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.Publisher) { + super.init() + browserIsLoading.sink { [weak self] (isLoading: Bool?) in + guard let self else { return } + switch isLoading { + case .none: + updateTo(.loadingData) + case .some(true): + if !wasLoaded { + updateTo(.loadingData) + } else { + updateTo(.webPage(isLoading: true)) + } + case .some(false): + wasLoaded = true + updateTo(.webPage(isLoading: false)) + } + }.store(in: &cancellables) + } +} + +// MARK: With Catalog Library +@MainActor +final class CatalogLaunchViewModel: LaunchViewModelBase { + + convenience init(hasZIMFiles: Published.Publisher, + library: LibraryViewModel, + browser: BrowserViewModel) { + self.init(hasZIMFiles: hasZIMFiles, + libraryState: library.$state, + browserIsLoading: browser.$isLoading, + hasSeenCategories: { Defaults[.hasSeenCategories] }) + } + + init(hasZIMFiles: Published.Publisher, + libraryState: Published.Publisher, + browserIsLoading: Published.Publisher, + hasSeenCategories: @escaping () -> Bool) { + super.init() + + hasZIMFiles.combineLatest( + libraryState, + browserIsLoading + ).sink { [weak self] ( + hasZIMs: Bool, + libState: LibraryState, + isBrowserLoading: Bool? + ) in + guard let 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(isCatalogLoading: true))) + } else { + updateTo(.catalog(.fetching)) + } + case (_, false, .complete): updateTo(.catalog(.welcome(isCatalogLoading: false))) + case (_, false, .error): updateTo(.catalog(.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) + } +} + +class LaunchViewModelBase: LaunchViewModelProtocol { + var state: Published.Publisher { $currentState } + var cancellables = Set() + @Published private var currentState: LaunchSequence = .loadingData + + func updateTo(_ newState: LaunchSequence) { + guard newState != currentState else { return } + currentState = newState + } +} From e871eb51194ff9948ad70f2e5a517cde120ce503 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 14 Oct 2024 01:20:22 +0200 Subject: [PATCH 04/18] Working LaunchViewModel --- App/CompactViewController.swift | 25 +++++++++++++++--- ViewModel/LaunchViewModel.swift | 46 +++++++++++++++++++++------------ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index ffce8882..19486957 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -133,6 +133,7 @@ final class CompactViewController: UIHostingController, 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 { @@ -151,6 +152,7 @@ private struct CompactView: View { if case .loading = navigation.currentItem { LoadingView() } else if case let .tab(tabID) = navigation.currentItem { + let browser = BrowserViewModel.getCached(tabID: tabID) Content(tabID: tabID, showLibrary: { if presentedSheet == nil { presentedSheet = .library @@ -158,7 +160,7 @@ private struct CompactView: View { // there's a sheet already presented by the user // do nothing } - }) + }, model: FeatureFlags.hasLibrary ? CatalogLaunchViewModel(library: library, browser: browser) : NoCatalogLaunchViewModel(browser: browser) ) .id(tabID) .toolbar { ToolbarItemGroup(placement: .bottomBar) { @@ -189,7 +191,7 @@ private struct CompactView: View { Spacer() } } - .environmentObject(BrowserViewModel.getCached(tabID: tabID)) + .environmentObject(browser) .sheet(item: $presentedSheet) { presentedSheet in switch presentedSheet { case .library: @@ -212,9 +214,10 @@ private struct CompactView: View { } } -private struct Content: View { +private struct Content: View where LaunchModel: LaunchProtocol { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var browser: BrowserViewModel + @EnvironmentObject private var library: LibraryViewModel @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], predicate: ZimFile.openedPredicate @@ -222,9 +225,21 @@ private struct Content: View { @State var isInitialLoad: Bool = true let tabID: NSManagedObjectID? let showLibrary: () -> Void + @ObservedObject var model: LaunchModel var body: some View { Group { + let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty) + let _ = debugPrint("model.state: \(model.state)") +// switch model.state { +// +// case .loadingData: +// Text("Loading data...") +// case .catalog(.list): +// Text("Catalog list") +// default: +// Text("") +// } if browser.url == nil || (!FeatureFlags.hasLibrary && isInitialLoad) { Welcome(showLibrary: showLibrary) } else { @@ -243,6 +258,10 @@ private struct Content: View { .onAppear { browser.updateLastOpened() } + .task { + debugPrint("library: \(library)") + debugPrint("") + } .onDisappear { // since the browser is comming from @Environment, // by the time we get to .onDisappear, diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index 440ed0cc..cde3cfb2 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -30,15 +30,15 @@ enum CatalogSequence: Equatable { case welcome(isCatalogLoading: Bool) } -protocol LaunchViewModelProtocol { - var state: Published.Publisher { get } +protocol LaunchProtocol: ObservableObject { + var state: LaunchSequence { get } + func updateWith(hasZimFiles: Bool) } // MARK: No Library (custom apps) /// Keeps us int the .loadingData state, /// while the main page is not fully loaded for the first time -@MainActor final class NoCatalogLaunchViewModel: LaunchViewModelBase { private var wasLoaded = false @@ -67,23 +67,25 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { } }.store(in: &cancellables) } + + override func updateWith(hasZimFiles: Bool) { + // to be ignored + } } // MARK: With Catalog Library -@MainActor final class CatalogLaunchViewModel: LaunchViewModelBase { - convenience init(hasZIMFiles: Published.Publisher, - library: LibraryViewModel, + private var hasZIMFiles = CurrentValueSubject(false) + + convenience init(library: LibraryViewModel, browser: BrowserViewModel) { - self.init(hasZIMFiles: hasZIMFiles, - libraryState: library.$state, + self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading, hasSeenCategories: { Defaults[.hasSeenCategories] }) } - init(hasZIMFiles: Published.Publisher, - libraryState: Published.Publisher, + init(libraryState: Published.Publisher, browserIsLoading: Published.Publisher, hasSeenCategories: @escaping () -> Bool) { super.init() @@ -110,7 +112,12 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { } else { updateTo(.catalog(.fetching)) } - case (_, false, .complete): updateTo(.catalog(.welcome(isCatalogLoading: false))) + case (_, false, .complete): + if hasSeenCategories() { + updateTo(.catalog(.welcome(isCatalogLoading: false))) + } else { + updateTo(.catalog(.fetching)) + } case (_, false, .error): updateTo(.catalog(.error)) // MARK: has zims and opens a new empty tab @@ -122,15 +129,22 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { } }.store(in: &cancellables) } + + override func updateWith(hasZimFiles: Bool) { + hasZIMFiles.send(hasZimFiles) + } } -class LaunchViewModelBase: LaunchViewModelProtocol { - var state: Published.Publisher { $currentState } +class LaunchViewModelBase: LaunchProtocol, ObservableObject { + var state: LaunchSequence = .loadingData var cancellables = Set() - @Published private var currentState: LaunchSequence = .loadingData func updateTo(_ newState: LaunchSequence) { - guard newState != currentState else { return } - currentState = newState + guard newState != state else { return } + state = newState + } + + func updateWith(hasZimFiles: Bool) { + fatalError("should be overriden") } } From f5f511d2c8a6f95aadc0f925538c5cc72cad257b Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 14 Oct 2024 01:54:38 +0200 Subject: [PATCH 05/18] WIP to work around Defaults --- App/CompactViewController.swift | 5 +++++ ViewModel/LaunchViewModel.swift | 16 ++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 19486957..eebf8ef9 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -22,6 +22,7 @@ import Combine import SwiftUI import UIKit import CoreData +import Defaults final class CompactViewController: UIHostingController, UISearchControllerDelegate, UISearchResultsUpdating { private let searchViewModel: SearchViewModel @@ -223,6 +224,10 @@ private struct Content: View where LaunchModel: LaunchProtocol { predicate: ZimFile.openedPredicate ) private var zimFiles: FetchedResults @State var isInitialLoad: Bool = true + + /// 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 diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index cde3cfb2..366c73dd 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -82,21 +82,25 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { browser: BrowserViewModel) { self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading, - hasSeenCategories: { Defaults[.hasSeenCategories] }) + hasSeenCategories: Default(.hasSeenCategories).publisher + ) } init(libraryState: Published.Publisher, browserIsLoading: Published.Publisher, - hasSeenCategories: @escaping () -> Bool) { + hasSeenCategories: Published.Publisher + ) { super.init() hasZIMFiles.combineLatest( libraryState, - browserIsLoading + browserIsLoading, + hasSeenCategories ).sink { [weak self] ( hasZIMs: Bool, libState: LibraryState, - isBrowserLoading: Bool? + isBrowserLoading: Bool?, + hasSeenCategories: Bool ) in guard let self else { return } @@ -107,13 +111,13 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { // MARK: browser must be empty as there are no ZIMs: case (_, false, .inProgress): - if hasSeenCategories() { + if hasSeenCategories { updateTo(.catalog(.welcome(isCatalogLoading: true))) } else { updateTo(.catalog(.fetching)) } case (_, false, .complete): - if hasSeenCategories() { + if hasSeenCategories { updateTo(.catalog(.welcome(isCatalogLoading: false))) } else { updateTo(.catalog(.fetching)) From 4b446858d57d03c602c27afeceff382edac45714 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 15 Oct 2024 23:27:03 +0200 Subject: [PATCH 06/18] Add Defaults as a param --- App/CompactViewController.swift | 19 +++++++++---------- ViewModel/LaunchViewModel.swift | 11 ++++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index eebf8ef9..290939be 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -154,6 +154,14 @@ private struct CompactView: View { LoadingView() } else if case let .tab(tabID) = navigation.currentItem { let browser = BrowserViewModel.getCached(tabID: tabID) + let model = if FeatureFlags.hasLibrary { + CatalogLaunchViewModel(library: library, + browser: browser, + hasSeenCategories: Defaults.publisher(.hasSeenCategories) + ) + } else { + NoCatalogLaunchViewModel(browser: browser) + } Content(tabID: tabID, showLibrary: { if presentedSheet == nil { presentedSheet = .library @@ -161,7 +169,7 @@ private struct CompactView: View { // there's a sheet already presented by the user // do nothing } - }, model: FeatureFlags.hasLibrary ? CatalogLaunchViewModel(library: library, browser: browser) : NoCatalogLaunchViewModel(browser: browser) ) + }, model: model) .id(tabID) .toolbar { ToolbarItemGroup(placement: .bottomBar) { @@ -236,15 +244,6 @@ private struct Content: View where LaunchModel: LaunchProtocol { Group { let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty) let _ = debugPrint("model.state: \(model.state)") -// switch model.state { -// -// case .loadingData: -// Text("Loading data...") -// case .catalog(.list): -// Text("Catalog list") -// default: -// Text("") -// } if browser.url == nil || (!FeatureFlags.hasLibrary && isInitialLoad) { Welcome(showLibrary: showLibrary) } else { diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index 366c73dd..f5d02422 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -69,7 +69,7 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { } override func updateWith(hasZimFiles: Bool) { - // to be ignored + // to be ignored on purpose } } @@ -79,16 +79,17 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { private var hasZIMFiles = CurrentValueSubject(false) convenience init(library: LibraryViewModel, - browser: BrowserViewModel) { + browser: BrowserViewModel, + hasSeenCategories: AnyPublisher, Never> + ) { self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading, - hasSeenCategories: Default(.hasSeenCategories).publisher - ) + hasSeenCategories: hasSeenCategories.map(\.newValue)) } init(libraryState: Published.Publisher, browserIsLoading: Published.Publisher, - hasSeenCategories: Published.Publisher + hasSeenCategories: Publishers.Map, Never>, Bool> ) { super.init() From 35963a3dc5810725d92a07ef81972951cbe6c371 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 15 Oct 2024 23:38:36 +0200 Subject: [PATCH 07/18] Move hasSeen to be a param --- App/CompactViewController.swift | 8 +++----- ViewModel/LaunchViewModel.swift | 24 ++++++++++-------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 290939be..7b45b4eb 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -155,10 +155,7 @@ private struct CompactView: View { } else if case let .tab(tabID) = navigation.currentItem { let browser = BrowserViewModel.getCached(tabID: tabID) let model = if FeatureFlags.hasLibrary { - CatalogLaunchViewModel(library: library, - browser: browser, - hasSeenCategories: Defaults.publisher(.hasSeenCategories) - ) + CatalogLaunchViewModel(library: library, browser: browser) } else { NoCatalogLaunchViewModel(browser: browser) } @@ -242,7 +239,8 @@ private struct Content: View where LaunchModel: LaunchProtocol { var body: some View { Group { - let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty) + let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, + hasSeenCategories: hasSeenCategories) let _ = debugPrint("model.state: \(model.state)") if browser.url == nil || (!FeatureFlags.hasLibrary && isInitialLoad) { Welcome(showLibrary: showLibrary) diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index f5d02422..8de94de0 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -32,7 +32,7 @@ enum CatalogSequence: Equatable { protocol LaunchProtocol: ObservableObject { var state: LaunchSequence { get } - func updateWith(hasZimFiles: Bool) + func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) } // MARK: No Library (custom apps) @@ -68,7 +68,7 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { }.store(in: &cancellables) } - override func updateWith(hasZimFiles: Bool) { + override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) { // to be ignored on purpose } } @@ -77,26 +77,21 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { final class CatalogLaunchViewModel: LaunchViewModelBase { private var hasZIMFiles = CurrentValueSubject(false) + private var hasSeenCategoriesOnce = CurrentValueSubject(false) convenience init(library: LibraryViewModel, - browser: BrowserViewModel, - hasSeenCategories: AnyPublisher, Never> - ) { - self.init(libraryState: library.$state, - browserIsLoading: browser.$isLoading, - hasSeenCategories: hasSeenCategories.map(\.newValue)) + browser: BrowserViewModel) { + self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading) } init(libraryState: Published.Publisher, - browserIsLoading: Published.Publisher, - hasSeenCategories: Publishers.Map, Never>, Bool> - ) { + browserIsLoading: Published.Publisher) { super.init() hasZIMFiles.combineLatest( libraryState, browserIsLoading, - hasSeenCategories + hasSeenCategoriesOnce ).sink { [weak self] ( hasZIMs: Bool, libState: LibraryState, @@ -135,8 +130,9 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { }.store(in: &cancellables) } - override func updateWith(hasZimFiles: Bool) { + override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) { hasZIMFiles.send(hasZimFiles) + hasSeenCategoriesOnce.send(hasSeenCategories) } } @@ -149,7 +145,7 @@ class LaunchViewModelBase: LaunchProtocol, ObservableObject { state = newState } - func updateWith(hasZimFiles: Bool) { + func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) { fatalError("should be overriden") } } From 7ad164d17884f1956c5b0451059e7e957c0e0e76 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Wed, 16 Oct 2024 00:26:45 +0200 Subject: [PATCH 08/18] Refactor Welcome --- App/CompactViewController.swift | 23 +++- ViewModel/LaunchViewModel.swift | 19 ++- Views/BuildingBlocks/LoadingView.swift | 13 +- Views/Welcome.swift | 179 +++++++++++++++---------- 4 files changed, 148 insertions(+), 86 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 7b45b4eb..03bfebbd 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -151,7 +151,7 @@ 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 { @@ -242,15 +242,26 @@ private struct Content: View where LaunchModel: LaunchProtocol { let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, hasSeenCategories: hasSeenCategories) let _ = debugPrint("model.state: \(model.state)") - if browser.url == nil || (!FeatureFlags.hasLibrary && isInitialLoad) { - Welcome(showLibrary: showLibrary) - } else { - WebView().ignoresSafeArea() + switch model.state { + case .loadingData: + LoadingDataView() + case .webPage(let isLoading): + WebView() + .ignoresSafeArea() .overlay { - if browser.isLoading == true { + if isLoading { LoadingProgressView() } } + case .catalog(let catalogSequence): + switch catalogSequence { + case .fetching: + FetchingCatalogView() + case .list: + LocalLibraryList() + case .welcome(let welcomeViewState): + WelcomeCatalog(viewState: welcomeViewState, showLibrary: showLibrary) + } } } .focusedSceneValue(\.browserViewModel, browser) diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index 8de94de0..13d0531e 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -25,9 +25,14 @@ enum LaunchSequence: Equatable { enum CatalogSequence: Equatable { case fetching - case error case list - case welcome(isCatalogLoading: Bool) + case welcome(WelcomeViewState) +} + +enum WelcomeViewState: Equatable { + case loading + case error + case complete } protocol LaunchProtocol: ObservableObject { @@ -108,17 +113,21 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { // MARK: browser must be empty as there are no ZIMs: case (_, false, .inProgress): if hasSeenCategories { - updateTo(.catalog(.welcome(isCatalogLoading: true))) + updateTo(.catalog(.welcome(.loading))) } else { updateTo(.catalog(.fetching)) } case (_, false, .complete): if hasSeenCategories { - updateTo(.catalog(.welcome(isCatalogLoading: false))) + updateTo(.catalog(.welcome(.complete))) } else { updateTo(.catalog(.fetching)) } - case (_, false, .error): updateTo(.catalog(.error)) + 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)) diff --git a/Views/BuildingBlocks/LoadingView.swift b/Views/BuildingBlocks/LoadingView.swift index c20a52b1..30af9a54 100644 --- a/Views/BuildingBlocks/LoadingView.swift +++ b/Views/BuildingBlocks/LoadingView.swift @@ -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() } diff --git a/Views/Welcome.swift b/Views/Welcome.swift index b9162934..8488e792 100644 --- a/Views/Welcome.swift +++ b/Views/Welcome.swift @@ -17,13 +17,9 @@ import SwiftUI import Combine import Defaults -struct Welcome: View { - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - @Environment(\.verticalSizeClass) private var verticalSizeClass + +struct LocalLibraryList: View { @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 @@ -33,68 +29,60 @@ struct Welcome: View { predicate: ZimFile.openedPredicate, animation: .easeInOut ) private var zimFiles: FetchedResults - /// Used only for iPhone - let showLibrary: (() -> Void)? var body: some View { - if zimFiles.isEmpty || !FeatureFlags.hasLibrary { - ZStack { - if !FeatureFlags.hasLibrary && browser.isLoading != false { - LoadingView() - } else { - 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) - } + 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) } - }.ignoresSafeArea() - } else if FeatureFlags.hasLibrary { - 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) + } + if !bookmarks.isEmpty { + GridSection(title: "welcome.grid.bookmarks.title".localized) { + ForEach(bookmarks.prefix(6)) { bookmark in + Button { + browser.load(url: bookmark.articleURL) } label: { - ZimFileCell(zimFile, prominent: .name) - } loading: { - ZimFileCell(zimFile, prominent: .name, isLoading: true) + ArticleCell(bookmark: bookmark) } .buttonStyle(.plain) + .modifier(BookmarkContextMenu(bookmark: bookmark)) } } - 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)) - } + } + }.modifier(GridCommon(edges: .all)) + } +} + +struct WelcomeCatalog: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.verticalSizeClass) private var verticalSizeClass + @EnvironmentObject private var navigation: NavigationViewModel + @EnvironmentObject private var library: LibraryViewModel + @Default(.hasSeenCategories) private var hasSeenCategories + let viewState: WelcomeViewState + + /// Used only for iPhone + let showLibrary: (() -> Void)? + + var body: some View { + ZStack { + LogoView() + welcomeContent + }.ignoresSafeArea() } private var welcomeContent: some View { @@ -109,7 +97,6 @@ struct Welcome: View { .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 { @@ -126,27 +113,28 @@ struct Welcome: View { } #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 - ) + 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 refresh catalog + /// Onboarding actions, open a zim file or refetch catalog private var actions: some View { if verticalSizeClass == .compact { // iPhone landscape AnyView(HStack { openFileButton - catalogButton + fetchCatalogButton }) } else { AnyView(VStack { openFileButton - catalogButton + fetchCatalogButton }) } } @@ -163,7 +151,7 @@ struct Welcome: View { .buttonStyle(.bordered) } - private var catalogButton: some View { + private var fetchCatalogButton: some View { Button { library.start(isUserInitiated: true) } label: { @@ -182,10 +170,55 @@ struct Welcome: View { .buttonStyle(.bordered) } } +// +//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 +// +// /// Used only for iPhone +// let showLibrary: (() -> Void)? +// +// var body: some View { +// if zimFiles.isEmpty || !FeatureFlags.hasLibrary { +// ZStack { +// if !FeatureFlags.hasLibrary && browser.isLoading != false { +// LoadingView() +// } else { +// 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 if FeatureFlags.hasLibrary { +// +// } +// } +// +// +//} 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() + WelcomeCatalog(viewState: .loading, + showLibrary: nil).environmentObject(LibraryViewModel()).preferredColorScheme(.light).padding() + WelcomeCatalog(viewState: .error, + showLibrary: nil).environmentObject(LibraryViewModel()).preferredColorScheme(.dark).padding() } } From d94c86211096244bb61d9c4f3e9dd3a7e4d4e1a2 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 09:25:38 +0200 Subject: [PATCH 09/18] Apply LaunchViewModel on BrowserTab --- App/CompactViewController.swift | 7 -- App/SplitViewController.swift | 2 +- Views/BrowserTab.swift | 78 +++++++++++++------ Views/LocalLibraryList.swift | 67 ++++++++++++++++ Views/{Welcome.swift => WelcomeCatalog.swift} | 50 ------------ 5 files changed, 124 insertions(+), 80 deletions(-) create mode 100644 Views/LocalLibraryList.swift rename Views/{Welcome.swift => WelcomeCatalog.swift} (75%) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 03bfebbd..a810a74d 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -228,8 +228,6 @@ private struct Content: View where LaunchModel: LaunchProtocol { sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], predicate: ZimFile.openedPredicate ) private var zimFiles: FetchedResults - @State var isInitialLoad: Bool = true - /// 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 @@ -300,11 +298,6 @@ private struct Content: View where LaunchModel: LaunchProtocol { browser.refreshVideoState() } } - .onChange(of: browser.isLoading) { isLoading in - if isLoading == false { // wait for the first full webpage load - isInitialLoad = false - } - } } } #endif diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 85f51973..cefd138e 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -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")) diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index 23d0f307..1d84cb10 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -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,23 @@ struct BrowserTab: View { } } - struct Content: View { + private struct Content: View where LaunchModel: LaunchProtocol { @Environment(\.isSearching) private var isSearching @EnvironmentObject private var browser: BrowserViewModel + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], + predicate: ZimFile.openedPredicate + ) private var zimFiles: FetchedResults + /// 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 { + let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, + hasSeenCategories: hasSeenCategories) + let _ = debugPrint("model.state: \(model.state)") GeometryReader { proxy in Group { if isSearching { @@ -92,17 +111,32 @@ 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, showLibrary: nil) + } } } } diff --git a/Views/LocalLibraryList.swift b/Views/LocalLibraryList.swift new file mode 100644 index 00000000..e3f2a17b --- /dev/null +++ b/Views/LocalLibraryList.swift @@ -0,0 +1,67 @@ +// 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 LocalLibraryList: View { + @EnvironmentObject private var browser: BrowserViewModel + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.created, ascending: false)], + animation: .easeInOut + ) private var bookmarks: FetchedResults + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], + predicate: ZimFile.openedPredicate, + animation: .easeInOut + ) private var zimFiles: FetchedResults + + 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)) + } +} diff --git a/Views/Welcome.swift b/Views/WelcomeCatalog.swift similarity index 75% rename from Views/Welcome.swift rename to Views/WelcomeCatalog.swift index 8488e792..68c1c59d 100644 --- a/Views/Welcome.swift +++ b/Views/WelcomeCatalog.swift @@ -17,56 +17,6 @@ import SwiftUI import Combine import Defaults - -struct LocalLibraryList: View { - @EnvironmentObject private var browser: BrowserViewModel - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.created, ascending: false)], - animation: .easeInOut - ) private var bookmarks: FetchedResults - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)], - predicate: ZimFile.openedPredicate, - animation: .easeInOut - ) private var zimFiles: FetchedResults - - 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)) - } -} - struct WelcomeCatalog: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass From 79589e74bfeced03f95d953ef1d970b4bdf6b779 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 10:13:08 +0200 Subject: [PATCH 10/18] Reduce Welcome view --- App/CompactViewController.swift | 25 ++++++++--- Views/BrowserTab.swift | 13 +++++- Views/LocalLibraryList.swift | 1 + Views/WelcomeCatalog.swift | 75 +++------------------------------ 4 files changed, 39 insertions(+), 75 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index a810a74d..f775a6a3 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -222,12 +222,15 @@ private struct CompactView: View { private struct Content: 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 + /// 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 @@ -258,7 +261,7 @@ private struct Content: View where LaunchModel: LaunchProtocol { case .list: LocalLibraryList() case .welcome(let welcomeViewState): - WelcomeCatalog(viewState: welcomeViewState, showLibrary: showLibrary) + WelcomeCatalog(viewState: welcomeViewState) } } } @@ -269,10 +272,6 @@ private struct Content: View where LaunchModel: LaunchProtocol { .onAppear { browser.updateLastOpened() } - .task { - debugPrint("library: \(library)") - debugPrint("") - } .onDisappear { // since the browser is comming from @Environment, // by the time we get to .onDisappear, @@ -298,6 +297,22 @@ private struct Content: View where LaunchModel: LaunchProtocol { browser.refreshVideoState() } } + .onChange(of: library.state) { state in + guard state == .complete else { return } + showTheLibrary() + } + } + + private func showTheLibrary() { + #if os(macOS) + navigation.currentItem = .categories + #else + if horizontalSizeClass == .regular { + navigation.currentItem = .categories + } else { + showLibrary() + } + #endif } } #endif diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index 1d84cb10..caef7a73 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -87,7 +87,10 @@ struct BrowserTab: View { private struct Content: 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 @@ -135,11 +138,19 @@ struct BrowserTab: View { case .catalog(.list): LocalLibraryList() case .catalog(.welcome(let welcomeViewState)): - WelcomeCatalog(viewState: welcomeViewState, showLibrary: nil) + WelcomeCatalog(viewState: welcomeViewState) } } } } + .onChange(of: library.state) { state in + guard state == .complete else { return } + showTheLibrary() + } + } + + private func showTheLibrary() { + navigation.currentItem = .categories } } } diff --git a/Views/LocalLibraryList.swift b/Views/LocalLibraryList.swift index e3f2a17b..01a0b9b5 100644 --- a/Views/LocalLibraryList.swift +++ b/Views/LocalLibraryList.swift @@ -17,6 +17,7 @@ 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( diff --git a/Views/WelcomeCatalog.swift b/Views/WelcomeCatalog.swift index 68c1c59d..87e71ce5 100644 --- a/Views/WelcomeCatalog.swift +++ b/Views/WelcomeCatalog.swift @@ -17,17 +17,14 @@ 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 navigation: NavigationViewModel @EnvironmentObject private var library: LibraryViewModel - @Default(.hasSeenCategories) private var hasSeenCategories let viewState: WelcomeViewState - /// Used only for iPhone - let showLibrary: (() -> Void)? - var body: some View { ZStack { LogoView() @@ -48,21 +45,6 @@ struct WelcomeCatalog: View { x: geometry.size.width * 0.5, y: logoCalc.buttonCenterY) .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 - } if viewState == .error { Text("library_refresh_error.retrieve.description".localized) .foregroundColor(.red) @@ -107,7 +89,7 @@ struct WelcomeCatalog: View { } label: { HStack { Spacer() - if library.state == .inProgress { + if viewState == .loading { Text("welcome.button.status.fetching_catalog.text".localized) } else { Text("welcome.button.status.fetch_catalog.text".localized) @@ -115,60 +97,15 @@ struct WelcomeCatalog: View { Spacer() }.padding(6) } - .disabled(library.state == .inProgress) + .disabled(viewState == .loading) .font(.subheadline) .buttonStyle(.bordered) } } -// -//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 -// -// /// Used only for iPhone -// let showLibrary: (() -> Void)? -// -// var body: some View { -// if zimFiles.isEmpty || !FeatureFlags.hasLibrary { -// ZStack { -// if !FeatureFlags.hasLibrary && browser.isLoading != false { -// LoadingView() -// } else { -// 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 if FeatureFlags.hasLibrary { -// -// } -// } -// -// -//} struct WelcomeView_Previews: PreviewProvider { static var previews: some View { - WelcomeCatalog(viewState: .loading, - showLibrary: nil).environmentObject(LibraryViewModel()).preferredColorScheme(.light).padding() - WelcomeCatalog(viewState: .error, - showLibrary: nil).environmentObject(LibraryViewModel()).preferredColorScheme(.dark).padding() + WelcomeCatalog(viewState: .loading).environmentObject(LibraryViewModel()).preferredColorScheme(.light).padding() + WelcomeCatalog(viewState: .error).environmentObject(LibraryViewModel()).preferredColorScheme(.dark).padding() } } From 6db51b9ccb75e45e712ae03d0b506d4739209065 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 15:33:31 +0200 Subject: [PATCH 11/18] Improve show catalog --- App/CompactViewController.swift | 1 + ViewModel/LaunchViewModel.swift | 10 ++++++++++ Views/BrowserTab.swift | 1 + 3 files changed, 12 insertions(+) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index f775a6a3..8567ebf7 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -304,6 +304,7 @@ private struct Content: View where LaunchModel: LaunchProtocol { } private func showTheLibrary() { + guard model.state.shouldShowCatalog else { return } #if os(macOS) navigation.currentItem = .categories #else diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index 13d0531e..db402e87 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -21,6 +21,16 @@ 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 { diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index caef7a73..4ee29f6d 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -150,6 +150,7 @@ struct BrowserTab: View { } private func showTheLibrary() { + guard model.state.shouldShowCatalog else { return } navigation.currentItem = .categories } } From 246eadb1dbbc98663a926f889234114973a5f122 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 16:39:50 +0200 Subject: [PATCH 12/18] Update macOS --- App/App_macOS.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 7ddbcb2e..59c94ef9 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -127,7 +127,7 @@ struct RootView: View { } detail: { switch navigation.currentItem { case .loading: - LoadingView() + LoadingDataView() case .reading: BrowserTab().environmentObject(browser) .withHostingWindow { window in From f915b79fbcee94c1e186b4299d9c7393971ae79a Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 17:34:53 +0200 Subject: [PATCH 13/18] Update custom app sequence --- ViewModel/LaunchViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index db402e87..02aef9e0 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -56,7 +56,7 @@ protocol LaunchProtocol: ObservableObject { /// while the main page is not fully loaded for the first time final class NoCatalogLaunchViewModel: LaunchViewModelBase { - private var wasLoaded = false + private static var wasLoaded = false convenience init(browser: BrowserViewModel) { self.init(browserIsLoading: browser.$isLoading) @@ -71,13 +71,13 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { case .none: updateTo(.loadingData) case .some(true): - if !wasLoaded { + if !Self.wasLoaded { updateTo(.loadingData) } else { updateTo(.webPage(isLoading: true)) } case .some(false): - wasLoaded = true + Self.wasLoaded = true updateTo(.webPage(isLoading: false)) } }.store(in: &cancellables) From 7608026b8f2cc7f7695ce84d9a7945f2db124dd9 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 17:51:23 +0200 Subject: [PATCH 14/18] Remove debug statement --- Views/BrowserTab.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index 4ee29f6d..124ff6f0 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -104,7 +104,6 @@ struct BrowserTab: View { var body: some View { let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, hasSeenCategories: hasSeenCategories) - let _ = debugPrint("model.state: \(model.state)") GeometryReader { proxy in Group { if isSearching { From 0c890677f1e5db69ed3621afbca72c6b4c76ba38 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 17:53:20 +0200 Subject: [PATCH 15/18] Fixlint --- ViewModel/LaunchViewModel.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index 02aef9e0..f24602b6 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -25,9 +25,9 @@ enum LaunchSequence: Equatable { var shouldShowCatalog: Bool { switch self { case .loadingData: return true - case .webPage(_): return false + case .webPage: return false case .catalog(.fetching): return true - case .catalog(.welcome(_)): return true + case .catalog(.welcome): return true case .catalog(.list): return false } } @@ -107,12 +107,7 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { libraryState, browserIsLoading, hasSeenCategoriesOnce - ).sink { [weak self] ( - hasZIMs: Bool, - libState: LibraryState, - isBrowserLoading: Bool?, - hasSeenCategories: Bool - ) in + ).sink { [weak self] (hasZIMs: Bool, libState: LibraryState, isBrowserLoading: Bool?, hasSeenCategories: Bool) in guard let self else { return } switch (isBrowserLoading, hasZIMs, libState) { From a7299075bf0004765ed8951bc8e92760444b870f Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 21:48:24 +0200 Subject: [PATCH 16/18] Fixlint --- ViewModel/BrowserViewModel.swift | 3 ++- Views/BrowserTab.swift | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 84e615ca..ae014f88 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -100,6 +100,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // MARK: - Lifecycle + // swiftlint:disable:next function_body_length @MainActor init(tabID: NSManagedObjectID? = nil) { self.tabID = tabID @@ -164,7 +165,7 @@ final class BrowserViewModel: NSObject, ObservableObject, self?.didUpdate(title: title, url: url) } - isLoadingObserver = webView.observe(\.isLoading, options: .new) { [weak self] webView, change in + isLoadingObserver = webView.observe(\.isLoading, options: .new) { [weak self] _, change in Task { @MainActor in if change.newValue != self?.isLoading { self?.isLoading = change.newValue diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index 124ff6f0..7fe1941e 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -100,7 +100,6 @@ struct BrowserTab: View { @Default(.hasSeenCategories) private var hasSeenCategories @ObservedObject var model: LaunchModel - var body: some View { let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, hasSeenCategories: hasSeenCategories) From 83765c68cd6a710152561fc3fa9c7f2d97fcfa28 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 22:22:15 +0200 Subject: [PATCH 17/18] Fixlint --- App/CompactViewController.swift | 2 +- ViewModel/BrowserViewModel.swift | 3 +-- ViewModel/LaunchViewModel.swift | 13 ++++++++++--- Views/BrowserTab.swift | 7 +++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 8567ebf7..5250adb6 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -240,9 +240,9 @@ private struct Content: View where LaunchModel: LaunchProtocol { var body: some View { Group { + // swiftlint:disable:next redundant_discardable_let let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, hasSeenCategories: hasSeenCategories) - let _ = debugPrint("model.state: \(model.state)") switch model.state { case .loadingData: LoadingDataView() diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index ae014f88..37477431 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -101,8 +101,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // MARK: - Lifecycle // swiftlint:disable:next function_body_length - @MainActor - init(tabID: NSManagedObjectID? = nil) { + @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, *) { diff --git a/ViewModel/LaunchViewModel.swift b/ViewModel/LaunchViewModel.swift index f24602b6..2c8aea0a 100644 --- a/ViewModel/LaunchViewModel.swift +++ b/ViewModel/LaunchViewModel.swift @@ -66,7 +66,7 @@ final class NoCatalogLaunchViewModel: LaunchViewModelBase { init(browserIsLoading: Published.Publisher) { super.init() browserIsLoading.sink { [weak self] (isLoading: Bool?) in - guard let self else { return } + guard let self = self else { return } switch isLoading { case .none: updateTo(.loadingData) @@ -99,6 +99,7 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading) } + // swiftlint:disable closure_parameter_position init(libraryState: Published.Publisher, browserIsLoading: Published.Publisher) { super.init() @@ -107,8 +108,13 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { libraryState, browserIsLoading, hasSeenCategoriesOnce - ).sink { [weak self] (hasZIMs: Bool, libState: LibraryState, isBrowserLoading: Bool?, hasSeenCategories: Bool) in - guard let self else { return } + ).sink { [weak self] ( + hasZIMs: Bool, + libState: LibraryState, + isBrowserLoading: Bool?, + hasSeenCategories: Bool + ) in + guard let self = self else { return } switch (isBrowserLoading, hasZIMs, libState) { @@ -143,6 +149,7 @@ final class CatalogLaunchViewModel: LaunchViewModelBase { } }.store(in: &cancellables) } + // swiftlint:enable closure_parameter_position override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) { hasZIMFiles.send(hasZimFiles) diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index 7fe1941e..25e191a5 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -101,8 +101,9 @@ struct BrowserTab: View { @ObservedObject var model: LaunchModel var body: some View { + // swiftlint:disable:next redundant_discardable_let let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty, - hasSeenCategories: hasSeenCategories) + hasSeenCategories: hasSeenCategories) GeometryReader { proxy in Group { if isSearching { @@ -127,7 +128,9 @@ struct BrowserTab: View { #if os(macOS) .overlay(alignment: .bottomTrailing) { ContentSearchBar( - model: ContentSearchViewModel(findInWebPage: browser.webView.find(_:configuration:)) + model: ContentSearchViewModel( + findInWebPage: browser.webView.find(_:configuration:) + ) ) } #endif From ba3aef17b4e4ad2c96297aa994dd6ac421e8f627 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Thu, 17 Oct 2024 22:35:20 +0200 Subject: [PATCH 18/18] Fix captured self --- ViewModel/BrowserViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 37477431..40dc48a3 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -165,7 +165,7 @@ final class BrowserViewModel: NSObject, ObservableObject, } isLoadingObserver = webView.observe(\.isLoading, options: .new) { [weak self] _, change in - Task { @MainActor in + Task { @MainActor [weak self] in if change.newValue != self?.isLoading { self?.isLoading = change.newValue }