mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-08 20:01:15 -04:00
233 lines
9.9 KiB
Swift
233 lines
9.9 KiB
Swift
// 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 Defaults
|
|
import CoreData
|
|
|
|
/// This is macOS and iPad only specific, not used on iPhone
|
|
struct BrowserTab: View {
|
|
@ObservedObject var browser: BrowserViewModel
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@EnvironmentObject private var library: LibraryViewModel
|
|
@StateObject private var search = SearchViewModel.shared
|
|
|
|
init(tabID: NSManagedObjectID) {
|
|
self.browser = BrowserViewModel.getCached(tabID: tabID)
|
|
}
|
|
|
|
var body: some View {
|
|
let model = if FeatureFlags.hasLibrary {
|
|
CatalogLaunchViewModel(library: library, browser: browser)
|
|
} else {
|
|
NoCatalogLaunchViewModel(browser: browser)
|
|
}
|
|
Content(browser: browser, model: model).toolbar {
|
|
#if os(macOS)
|
|
ToolbarItemGroup(placement: .navigation) {
|
|
NavigationButtons(
|
|
goBack: { [weak browser] in
|
|
browser?.webView.goBack()
|
|
},
|
|
goForward: { [weak browser] in
|
|
browser?.webView.goForward()
|
|
})
|
|
}
|
|
#elseif os(iOS)
|
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
|
if #unavailable(iOS 16) {
|
|
Button {
|
|
NotificationCenter.toggleSidebar()
|
|
} label: {
|
|
Label(LocalString.browser_tab_toolbar_show_sidebar_label, systemImage: "sidebar.left")
|
|
}
|
|
}
|
|
NavigationButtons(
|
|
goBack: { [weak browser] in
|
|
browser?.webView.goBack()
|
|
},
|
|
goForward: { [weak browser] in
|
|
browser?.webView.goForward()
|
|
})
|
|
}
|
|
#endif
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
if !Brand.hideTOCButton {
|
|
OutlineButton(browser: browser)
|
|
}
|
|
#if os(iOS)
|
|
if !Brand.hideShareButton {
|
|
ExportButton(
|
|
webViewURL: browser.webView.url,
|
|
pageDataWithExtension: { [weak browser] in await browser?.pageDataWithExtension() },
|
|
isButtonDisabled: browser.zimFileName.isEmpty
|
|
)
|
|
}
|
|
#else
|
|
if !Brand.hideShareButton {
|
|
Menu {
|
|
ExportButton(
|
|
relativeToView: browser.webView,
|
|
webViewURL: browser.webView.url,
|
|
pageDataWithExtension: { [weak browser] in await browser?.pageDataWithExtension() },
|
|
isButtonDisabled: browser.zimFileName.isEmpty,
|
|
buttonLabel: LocalString.common_button_share_as_pdf
|
|
)
|
|
if let url = browser.webView.url {
|
|
CopyPasteMenu(url: url)
|
|
.keyboardShortcut("c", modifiers: [.command, .shift])
|
|
}
|
|
} label: {
|
|
Label(LocalString.common_button_share, systemImage: "square.and.arrow.up")
|
|
}.disabled(browser.webView.url == nil)
|
|
}
|
|
if !Brand.hidePrintButton {
|
|
PrintButton(browserURLName: { [weak browser] in
|
|
browser?.url?.lastPathComponent
|
|
}, browserDataAsPDF: { [weak browser] in
|
|
try await browser?.webView.pdf()
|
|
})
|
|
}
|
|
#endif
|
|
BookmarkButton(articleBookmarked: browser.articleBookmarked,
|
|
isButtonDisabled: browser.zimFileName.isEmpty,
|
|
createBookmark: { [weak browser] in browser?.createBookmark() },
|
|
deleteBookmark: { [weak browser] in browser?.deleteBookmark() })
|
|
#if os(iOS)
|
|
if !Brand.hideFindInPage {
|
|
ContentSearchButton(browser: browser)
|
|
}
|
|
#endif
|
|
ArticleShortcutButtons(
|
|
loadMainArticle: { [weak browser] zimFileID in
|
|
browser?.loadMainArticle(zimFileID: zimFileID)
|
|
},
|
|
loadRandomArticle: { [weak browser] zimFileID in
|
|
browser?.loadRandomArticle(zimFileID: zimFileID)
|
|
})
|
|
}
|
|
}
|
|
.environmentObject(search)
|
|
.focusedSceneValue(\.isBrowserURLSet, browser.url != nil)
|
|
#if os(macOS)
|
|
.focusedSceneValue(\.browserURL, browser.url)
|
|
#endif
|
|
.focusedSceneValue(\.canGoBack, browser.canGoBack)
|
|
.focusedSceneValue(\.canGoForward, browser.canGoForward)
|
|
.modifier(ExternalLinkHandler(externalURL: $browser.externalURL))
|
|
.searchable(text: $search.searchText, placement: .toolbar, prompt: LocalString.common_search)
|
|
.onChange(of: scenePhase) { [weak browser] newValue in
|
|
if case .active = newValue {
|
|
browser?.refreshVideoState()
|
|
}
|
|
}
|
|
.modify { [weak browser] view in
|
|
#if os(macOS)
|
|
if let browser {
|
|
view.navigationTitle(browser.articleTitle.isEmpty ? Brand.appName : browser.articleTitle)
|
|
.navigationSubtitle(browser.zimFileName)
|
|
} else {
|
|
view
|
|
}
|
|
#elseif os(iOS)
|
|
view
|
|
#endif
|
|
}
|
|
.onAppear { [weak browser] in
|
|
browser?.updateLastOpened()
|
|
}
|
|
.onDisappear { [weak browser] in
|
|
browser?.pauseVideoWhenNotInPIP()
|
|
browser?.persistState()
|
|
}
|
|
}
|
|
|
|
private struct Content<LaunchModel>: View where LaunchModel: LaunchProtocol {
|
|
@Environment(\.isSearching) private var isSearching
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
let 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 {
|
|
SearchResults()
|
|
#if os(macOS)
|
|
.environment(\.horizontalSizeClass, proxy.size.width > 650 ? .regular : .compact)
|
|
#elseif os(iOS)
|
|
.environment(\.horizontalSizeClass, proxy.size.width > 750 ? .regular : .compact)
|
|
#endif
|
|
} else {
|
|
switch model.state {
|
|
case .loadingData, .webPage:
|
|
ZStack {
|
|
LoadingDataView()
|
|
.opacity(model.state == .loadingData ? 1.0 : 0.0)
|
|
WebView(browser: browser)
|
|
.opacity(model.state == .loadingData ? 0.0 : 1.0)
|
|
.ignoresSafeArea()
|
|
.overlay {
|
|
if case .webPage(let isLoading) = model.state, isLoading {
|
|
LoadingProgressView()
|
|
}
|
|
}
|
|
#if os(macOS)
|
|
.overlay(alignment: .bottomTrailing) {
|
|
if !Brand.hideFindInPage {
|
|
ContentSearchBar(
|
|
model: ContentSearchViewModel(
|
|
findInWebPage: browser.webView.find(_:configuration:)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
case .catalog(.fetching):
|
|
FetchingCatalogView()
|
|
case .catalog(.list):
|
|
LocalLibraryList(browser: browser)
|
|
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
|
|
}
|
|
}
|
|
}
|