Merge pull request #1008 from kiwix/1006-improve-launch-sequence-for-custom-apps

Improve launch sequence for custom apps
This commit is contained in:
Kelson 2024-10-18 08:57:28 +02:00 committed by GitHub
commit c78d229dac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 507 additions and 223 deletions

View File

@ -127,7 +127,7 @@ struct RootView: View {
} detail: {
switch navigation.currentItem {
case .loading:
LoadingView()
LoadingDataView()
case .reading:
BrowserTab().environmentObject(browser)
.withHostingWindow { window in

View File

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

View File

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

View File

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

View 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")
}
}

View File

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

View File

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

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

View File

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