Merge pull request #576 from kiwix/feature/520-custom-app-support-v2

Add custom app support, fixing all the core issues in apple code baseCustom app support
This commit is contained in:
Kelson 2023-12-04 20:27:38 +01:00 committed by GitHub
commit b4c944e96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 369 additions and 159 deletions

View File

@ -1,4 +1,3 @@
//
// App_iOS.swift // App_iOS.swift
// Kiwix // Kiwix
// //
@ -21,13 +20,8 @@ struct Kiwix: App {
init() { init() {
fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) } fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) }
UNUserNotificationCenter.current().delegate = appDelegate
LibraryOperations.reopen()
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
LibraryOperations.registerBackgroundTask() LibraryOperations.registerBackgroundTask()
LibraryOperations.applyLibraryAutoRefreshSetting() UNUserNotificationCenter.current().delegate = appDelegate
DownloadService.shared.restartHeartbeatIfNeeded()
} }
var body: some Scene { var body: some Scene {
@ -50,6 +44,23 @@ struct Kiwix: App {
NotificationCenter.openURL(url) NotificationCenter.openURL(url)
} }
} }
.task {
switch AppType.current {
case .kiwix:
fileMonitor.start()
LibraryOperations.reopen {
navigation.navigateToMostRecentTab()
}
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
LibraryOperations.applyLibraryAutoRefreshSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
case let .custom(zimFileURL):
LibraryOperations.open(url: zimFileURL) {
navigation.navigateToMostRecentTab()
}
}
}
} }
.commands { .commands {
CommandGroup(replacing: .undoRedo) { CommandGroup(replacing: .undoRedo) {

View File

@ -10,6 +10,7 @@ import SwiftUI
import UserNotifications import UserNotifications
import Combine import Combine
import Defaults import Defaults
import CoreKiwix
#if os(macOS) #if os(macOS)
@main @main
@ -19,10 +20,6 @@ struct Kiwix: App {
init() { init() {
UNUserNotificationCenter.current().delegate = notificationCenterDelegate UNUserNotificationCenter.current().delegate = notificationCenterDelegate
LibraryOperations.reopen()
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
} }
var body: some Scene { var body: some Scene {
@ -57,11 +54,13 @@ struct Kiwix: App {
Settings { Settings {
TabView { TabView {
ReadingSettings() ReadingSettings()
LibrarySettings() if FeatureFlags.hasLibrary {
LibrarySettings()
.environmentObject(libraryRefreshViewModel)
}
About() About()
} }
.frame(width: 550, height: 400) .frame(width: 550, height: 400)
.environmentObject(libraryRefreshViewModel)
} }
} }
@ -95,9 +94,11 @@ struct RootView: View {
ForEach(primaryItems, id: \.self) { navigationItem in ForEach(primaryItems, id: \.self) { navigationItem in
Label(navigationItem.name.localized, systemImage: navigationItem.icon) Label(navigationItem.name.localized, systemImage: navigationItem.icon)
} }
Section("Library".localized) { if FeatureFlags.hasLibrary {
ForEach(libraryItems, id: \.self) { navigationItem in Section("Library".localized) {
Label(navigationItem.name.localized, systemImage: navigationItem.icon) ForEach(libraryItems, id: \.self) { navigationItem in
Label(navigationItem.name.localized, systemImage: navigationItem.icon)
}
} }
} }
} }
@ -111,6 +112,8 @@ struct RootView: View {
}.help("Show sidebar".localized) }.help("Show sidebar".localized)
} }
switch navigation.currentItem { switch navigation.currentItem {
case .loading:
LoadingView()
case .reading: case .reading:
BrowserTab().environmentObject(browser) BrowserTab().environmentObject(browser)
.withHostingWindow { window in .withHostingWindow { window in
@ -152,6 +155,20 @@ struct RootView: View {
} }
.onReceive(appTerminates) { _ in .onReceive(appTerminates) { _ in
browser.persistAllTabIdsFromWindows() browser.persistAllTabIdsFromWindows()
}.task {
switch AppType.current {
case .kiwix:
LibraryOperations.reopen {
navigation.currentItem = .reading
}
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
case let .custom(zimFileURL):
LibraryOperations.open(url: zimFileURL) {
navigation.currentItem = .reading
}
}
} }
} }
} }

View File

@ -44,6 +44,7 @@ class CompactViewController: UIHostingController<AnyView>, UISearchControllerDel
apperance.configureWithDefaultBackground() apperance.configureWithDefaultBackground()
return apperance return apperance
}() }()
searchController.searchBar.autocorrectionType = .no
navigationItem.titleView = searchController.searchBar navigationItem.titleView = searchController.searchBar
searchController.automaticallyShowsCancelButton = false searchController.automaticallyShowsCancelButton = false
searchController.delegate = self searchController.delegate = self
@ -54,10 +55,6 @@ class CompactViewController: UIHostingController<AnyView>, UISearchControllerDel
guard self?.searchController.searchBar.text != searchText else { return } guard self?.searchController.searchBar.text != searchText else { return }
self?.searchController.searchBar.text = searchText self?.searchController.searchBar.text = searchText
} }
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
openURLObserver = NotificationCenter.default.addObserver( openURLObserver = NotificationCenter.default.addObserver(
forName: .openURL, object: nil, queue: nil forName: .openURL, object: nil, queue: nil
) { [weak self] _ in ) { [weak self] _ in
@ -65,12 +62,11 @@ class CompactViewController: UIHostingController<AnyView>, UISearchControllerDel
self?.navigationItem.setRightBarButton(nil, animated: true) self?.navigationItem.setRightBarButton(nil, animated: true)
} }
} }
override func viewDidDisappear(_ animated: Bool) { deinit {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
func willPresentSearchController(_ searchController: UISearchController) { func willPresentSearchController(_ searchController: UISearchController) {
navigationController?.setToolbarHidden(true, animated: true) navigationController?.setToolbarHidden(true, animated: true)
navigationItem.setRightBarButton( navigationItem.setRightBarButton(

View File

@ -43,6 +43,14 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
case tabs case tabs
case library case library
case settings case settings
static var allSections: [Section] {
if FeatureFlags.hasLibrary {
allCases
} else {
allCases.filter { $0 != .library }
}
}
} }
init() { init() {
@ -77,7 +85,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
fetchedResultController.delegate = self fetchedResultController.delegate = self
// configure view // configure view
navigationItem.title = "Kiwix" navigationItem.title = Brand.appName
navigationItem.rightBarButtonItem = UIBarButtonItem( navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "plus.square"), image: UIImage(systemName: "plus.square"),
primaryAction: UIAction { [unowned self] _ in primaryAction: UIAction { [unowned self] _ in
@ -107,9 +115,11 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
// apply initial snapshot // apply initial snapshot
var snapshot = NSDiffableDataSourceSnapshot<Section, NavigationItem>() var snapshot = NSDiffableDataSourceSnapshot<Section, NavigationItem>()
snapshot.appendSections(Section.allCases) snapshot.appendSections(Section.allSections)
snapshot.appendItems([.bookmarks], toSection: .primary) snapshot.appendItems([.bookmarks], toSection: .primary)
snapshot.appendItems([.opened, .categories, .downloads, .new], toSection: .library) if FeatureFlags.hasLibrary {
snapshot.appendItems([.opened, .categories, .downloads, .new], toSection: .library)
}
snapshot.appendItems([.settings], toSection: .settings) snapshot.appendItems([.settings], toSection: .settings)
dataSource.apply(snapshot, animatingDifferences: false) dataSource.apply(snapshot, animatingDifferences: false)
try? fetchedResultController.performFetch() try? fetchedResultController.performFetch()
@ -184,7 +194,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl
} }
private func configureHeader(headerView: UICollectionViewListCell, elementKind: String, indexPath: IndexPath) { private func configureHeader(headerView: UICollectionViewListCell, elementKind: String, indexPath: IndexPath) {
let section = Section.allCases[indexPath.section] let section = Section.allSections[indexPath.section]
switch section { switch section {
case .tabs: case .tabs:
var config = UIListContentConfiguration.sidebarHeader() var config = UIListContentConfiguration.sidebarHeader()

View File

@ -11,7 +11,7 @@ import Combine
import SwiftUI import SwiftUI
import UIKit import UIKit
class SplitViewController: UISplitViewController { final class SplitViewController: UISplitViewController {
let navigationViewModel: NavigationViewModel let navigationViewModel: NavigationViewModel
private var navigationItemObserver: AnyCancellable? private var navigationItemObserver: AnyCancellable?
private var openURLObserver: NSObjectProtocol? private var openURLObserver: NSObjectProtocol?
@ -127,6 +127,9 @@ class SplitViewController: UISplitViewController {
case .settings: case .settings:
let controller = UIHostingController(rootView: Settings()) let controller = UIHostingController(rootView: Settings())
setViewController(UINavigationController(rootViewController: controller), for: .secondary) setViewController(UINavigationController(rootViewController: controller), for: .secondary)
case .loading:
let controller = UIHostingController(rootView: LoadingView())
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
default: default:
let controller = UIHostingController(rootView: Text("Not yet implemented".localized)) let controller = UIHostingController(rootView: Text("Not yet implemented".localized))
setViewController(UINavigationController(rootViewController: controller), for: .secondary) setViewController(UINavigationController(rootViewController: controller), for: .secondary)

View File

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 55; objectVersion = 60;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -44,7 +44,6 @@
973A0DE8281DD7EB00B41E71 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A7E224567A5A00F6F6FF /* Log.swift */; }; 973A0DE8281DD7EB00B41E71 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A7E224567A5A00F6F6FF /* Log.swift */; };
973A0DEB281DDBB600B41E71 /* ZimFilesDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DE9281DDBB600B41E71 /* ZimFilesDownloads.swift */; }; 973A0DEB281DDBB600B41E71 /* ZimFilesDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DE9281DDBB600B41E71 /* ZimFilesDownloads.swift */; };
973A0DF1282E981200B41E71 /* ZimFileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DEF282E981200B41E71 /* ZimFileRow.swift */; }; 973A0DF1282E981200B41E71 /* ZimFileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DEF282E981200B41E71 /* ZimFileRow.swift */; };
973A0DF72830929C00B41E71 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97E94B1D271EF250005B0295 /* Assets.xcassets */; };
973A0DFD283100C300B41E71 /* ZimFilesOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DFB283100C300B41E71 /* ZimFilesOpened.swift */; }; 973A0DFD283100C300B41E71 /* ZimFilesOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DFB283100C300B41E71 /* ZimFilesOpened.swift */; };
973A0E032831057200B41E71 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B3BACD2736CE3500A23F49 /* URL.swift */; }; 973A0E032831057200B41E71 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B3BACD2736CE3500A23F49 /* URL.swift */; };
9744068728CE263800916BD4 /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5CF2456796A00F6F6FF /* DirectoryMonitor.swift */; }; 9744068728CE263800916BD4 /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5CF2456796A00F6F6FF /* DirectoryMonitor.swift */; };
@ -99,9 +98,13 @@
97DE2BA3283A8E5C00C63D9B /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DE2BA1283A8E5C00C63D9B /* LibraryViewModel.swift */; }; 97DE2BA3283A8E5C00C63D9B /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DE2BA1283A8E5C00C63D9B /* LibraryViewModel.swift */; };
97DE2BA6283A944100C63D9B /* GridCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DE2BA4283A944100C63D9B /* GridCommon.swift */; }; 97DE2BA6283A944100C63D9B /* GridCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DE2BA4283A944100C63D9B /* GridCommon.swift */; };
97DE2BAD283B133700C63D9B /* wikipedia_dark.css in Resources */ = {isa = PBXBuildFile; fileRef = 970885D0271339A300C5795C /* wikipedia_dark.css */; }; 97DE2BAD283B133700C63D9B /* wikipedia_dark.css in Resources */ = {isa = PBXBuildFile; fileRef = 970885D0271339A300C5795C /* wikipedia_dark.css */; };
97E88F4D2AE407350037F0E5 /* CoreKiwix.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97E88F4C2AE407320037F0E5 /* CoreKiwix.xcframework */; }; 97E88F4D2AE407350037F0E5 /* CoreKiwix.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97E88F4C2AE407320037F0E5 /* CoreKiwix.xcframework */; settings = {ATTRIBUTES = (Required, ); }; };
97F3333028AFC1A2007FF53C /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F3332E28AFC1A2007FF53C /* SearchResults.swift */; }; 97F3333028AFC1A2007FF53C /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F3332E28AFC1A2007FF53C /* SearchResults.swift */; };
97FB4ECE28B4E221003FB524 /* SwiftUIBackports in Frameworks */ = {isa = PBXBuildFile; productRef = 97FB4ECD28B4E221003FB524 /* SwiftUIBackports */; }; 97FB4ECE28B4E221003FB524 /* SwiftUIBackports in Frameworks */ = {isa = PBXBuildFile; productRef = 97FB4ECD28B4E221003FB524 /* SwiftUIBackports */; };
980179512B0DF42100E8E1E2 /* Brand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980179502B0DF42100E8E1E2 /* Brand.swift */; };
980179542B0E402C00E8E1E2 /* kiwix.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 980179532B0E402C00E8E1E2 /* kiwix.xcconfig */; };
983A3F562B129F6B000E7A51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 983A3F552B129F6B000E7A51 /* Assets.xcassets */; };
983D22672B16ABB6005EBAF1 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983D22662B16ABB6005EBAF1 /* LoadingView.swift */; };
983ED7192B08AFE700409078 /* Kiwix-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5D02456796A00F6F6FF /* Kiwix-Bridging-Header.h */; platformFilter = ios; }; 983ED7192B08AFE700409078 /* Kiwix-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = 9779A5D02456796A00F6F6FF /* Kiwix-Bridging-Header.h */; platformFilter = ios; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -234,13 +237,16 @@
97DE2BA1283A8E5C00C63D9B /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; }; 97DE2BA1283A8E5C00C63D9B /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
97DE2BA4283A944100C63D9B /* GridCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridCommon.swift; sourceTree = "<group>"; }; 97DE2BA4283A944100C63D9B /* GridCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridCommon.swift; sourceTree = "<group>"; };
97E88F4C2AE407320037F0E5 /* CoreKiwix.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = CoreKiwix.xcframework; sourceTree = SOURCE_ROOT; }; 97E88F4C2AE407320037F0E5 /* CoreKiwix.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = CoreKiwix.xcframework; sourceTree = SOURCE_ROOT; };
97E94B1D271EF250005B0295 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97E94B22271EF250005B0295 /* Kiwix.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Kiwix.entitlements; sourceTree = "<group>"; }; 97E94B22271EF250005B0295 /* Kiwix.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Kiwix.entitlements; sourceTree = "<group>"; };
97F3332E28AFC1A2007FF53C /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = "<group>"; }; 97F3332E28AFC1A2007FF53C /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = "<group>"; };
97F425C127151A0D00D0F738 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; 97F425C127151A0D00D0F738 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; };
97F6CC5020BD960F005CDBD2 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; 97F6CC5020BD960F005CDBD2 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; };
97FB4B0A27B819A90055F86E /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; }; 97FB4B0A27B819A90055F86E /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
97FD2F5E251EA07B0034927C /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = "<group>"; }; 97FD2F5E251EA07B0034927C /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = "<group>"; };
980179502B0DF42100E8E1E2 /* Brand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brand.swift; sourceTree = "<group>"; };
980179532B0E402C00E8E1E2 /* kiwix.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = kiwix.xcconfig; sourceTree = "<group>"; };
983A3F552B129F6B000E7A51 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
983D22662B16ABB6005EBAF1 /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -317,6 +323,7 @@
974E7EE22930201500BDF59C /* ZimFileService */, 974E7EE22930201500BDF59C /* ZimFileService */,
9735D0B82775363900C7D495 /* DataModel.xcdatamodeld */, 9735D0B82775363900C7D495 /* DataModel.xcdatamodeld */,
973A0DE5281DC8F400B41E71 /* DownloadService.swift */, 973A0DE5281DC8F400B41E71 /* DownloadService.swift */,
980179502B0DF42100E8E1E2 /* Brand.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@ -407,6 +414,7 @@
975088BF287EEE2900273181 /* BuildingBlocks */ = { 975088BF287EEE2900273181 /* BuildingBlocks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
983D22662B16ABB6005EBAF1 /* LoadingView.swift */,
97486D05284A36790096E4DD /* ArticleCell.swift */, 97486D05284A36790096E4DD /* ArticleCell.swift */,
972096E62AE421C300B378B0 /* Attribute.swift */, 972096E62AE421C300B378B0 /* Attribute.swift */,
97341C6C2852248500BC273E /* DownloadTaskCell.swift */, 97341C6C2852248500BC273E /* DownloadTaskCell.swift */,
@ -553,7 +561,7 @@
97E94B26271EF359005B0295 /* Support */ = { 97E94B26271EF359005B0295 /* Support */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
97E94B1D271EF250005B0295 /* Assets.xcassets */, 983A3F552B129F6B000E7A51 /* Assets.xcassets */,
9713F7782AE416E5007DD9EC /* CoreKiwix.modulemap */, 9713F7782AE416E5007DD9EC /* CoreKiwix.modulemap */,
97B707042974637200562392 /* Info.plist */, 97B707042974637200562392 /* Info.plist */,
9735B88B279D9A74005F0D1A /* injection.js */, 9735B88B279D9A74005F0D1A /* injection.js */,
@ -561,6 +569,7 @@
9779A5D02456796A00F6F6FF /* Kiwix-Bridging-Header.h */, 9779A5D02456796A00F6F6FF /* Kiwix-Bridging-Header.h */,
970885D0271339A300C5795C /* wikipedia_dark.css */, 970885D0271339A300C5795C /* wikipedia_dark.css */,
8E4396492B02E455007F0BC4 /* Localizable.strings */, 8E4396492B02E455007F0BC4 /* Localizable.strings */,
980179532B0E402C00E8E1E2 /* kiwix.xcconfig */,
); );
path = Support; path = Support;
sourceTree = "<group>"; sourceTree = "<group>";
@ -638,7 +647,7 @@
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420; LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1500; LastUpgradeCheck = 1500;
ORGANIZATIONNAME = "Chris Li"; ORGANIZATIONNAME = Kiwix;
TargetAttributes = { TargetAttributes = {
97008AB92974A5BF0076E60C = { 97008AB92974A5BF0076E60C = {
CreatedOnToolsVersion = 14.2; CreatedOnToolsVersion = 14.2;
@ -693,9 +702,10 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
973A0DF72830929C00B41E71 /* Assets.xcassets in Resources */, 980179542B0E402C00E8E1E2 /* kiwix.xcconfig in Resources */,
8E4396462B02E455007F0BC4 /* Localizable.strings in Resources */, 8E4396462B02E455007F0BC4 /* Localizable.strings in Resources */,
979D3A7C284159BF00E396B8 /* injection.js in Resources */, 979D3A7C284159BF00E396B8 /* injection.js in Resources */,
983A3F562B129F6B000E7A51 /* Assets.xcassets in Resources */,
97DE2BAD283B133700C63D9B /* wikipedia_dark.css in Resources */, 97DE2BAD283B133700C63D9B /* wikipedia_dark.css in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -724,6 +734,7 @@
); );
inputPaths = ( inputPaths = (
); );
name = "Run Script";
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
@ -820,6 +831,7 @@
972DE4B42814A4F6004FD9B9 /* DataModel.xcdatamodeld in Sources */, 972DE4B42814A4F6004FD9B9 /* DataModel.xcdatamodeld in Sources */,
976BAEBD2849056B0049404F /* SearchOperation.swift in Sources */, 976BAEBD2849056B0049404F /* SearchOperation.swift in Sources */,
976D90E428159BFA00CC7D29 /* Favicon.swift in Sources */, 976D90E428159BFA00CC7D29 /* Favicon.swift in Sources */,
983D22672B16ABB6005EBAF1 /* LoadingView.swift in Sources */,
976D90DB281584BF00CC7D29 /* FlavorTag.swift in Sources */, 976D90DB281584BF00CC7D29 /* FlavorTag.swift in Sources */,
972DE4BD2814A5BE004FD9B9 /* OPDSParser.mm in Sources */, 972DE4BD2814A5BE004FD9B9 /* OPDSParser.mm in Sources */,
972DE4B52814A502004FD9B9 /* Entities.swift in Sources */, 972DE4B52814A502004FD9B9 /* Entities.swift in Sources */,
@ -841,6 +853,7 @@
973A0DE7281DC8F400B41E71 /* DownloadService.swift in Sources */, 973A0DE7281DC8F400B41E71 /* DownloadService.swift in Sources */,
9721BBB72841C16D005C910D /* Message.swift in Sources */, 9721BBB72841C16D005C910D /* Message.swift in Sources */,
97BD6C9D2886E06B004A8532 /* HTMLParser.swift in Sources */, 97BD6C9D2886E06B004A8532 /* HTMLParser.swift in Sources */,
980179512B0DF42100E8E1E2 /* Brand.swift in Sources */,
97677B572A8FA80000F523AB /* FileImport.swift in Sources */, 97677B572A8FA80000F523AB /* FileImport.swift in Sources */,
972727AA2A89122F00BCAF75 /* App_macOS.swift in Sources */, 972727AA2A89122F00BCAF75 /* App_macOS.swift in Sources */,
97DE2BA3283A8E5C00C63D9B /* LibraryViewModel.swift in Sources */, 97DE2BA3283A8E5C00C63D9B /* LibraryViewModel.swift in Sources */,
@ -961,6 +974,7 @@
}; };
972DE4B12814A3D9004FD9B9 /* Debug */ = { 972DE4B12814A3D9004FD9B9 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 980179532B0E402C00E8E1E2 /* kiwix.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -970,13 +984,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Support/Kiwix.entitlements; CODE_SIGN_ENTITLEMENTS = Support/Kiwix.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 119; CURRENT_PROJECT_VERSION = 120;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = L7HWM3SP3L; DEVELOPMENT_TEAM = L7HWM3SP3L;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Support/Info.plist; INFOPLIST_FILE = Support/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Kiwix;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Kiwix needs permission to saves images to your photos app."; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Kiwix needs permission to saves images to your photos app.";
@ -989,7 +1004,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 3.2; MARKETING_VERSION = 3.3;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix; PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
@ -1007,6 +1022,7 @@
}; };
972DE4B22814A3D9004FD9B9 /* Release */ = { 972DE4B22814A3D9004FD9B9 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 980179532B0E402C00E8E1E2 /* kiwix.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
@ -1016,18 +1032,20 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Support/Kiwix.entitlements; CODE_SIGN_ENTITLEMENTS = Support/Kiwix.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 119; CURRENT_PROJECT_VERSION = 120;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = L7HWM3SP3L; DEVELOPMENT_TEAM = L7HWM3SP3L;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Support/Info.plist; INFOPLIST_FILE = Support/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Kiwix;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Kiwix needs permission to saves images to your photos app."; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Kiwix needs permission to saves images to your photos app.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchKiwix.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -1035,7 +1053,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 3.2; MARKETING_VERSION = 3.3;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix; PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
PRODUCT_NAME = Kiwix; PRODUCT_NAME = Kiwix;

82
Model/Brand.swift Normal file
View File

@ -0,0 +1,82 @@
// Copyright © 2023 Kiwix.
import Foundation
import os
enum AppType {
case kiwix
case custom(zimFileURL: URL)
static let current = AppType()
static var isCustom: Bool {
switch current {
case .kiwix: return false
case .custom: return true
}
}
private init() {
guard let zimFileName: String = Config.value(for: .customZimFile),
!zimFileName.isEmpty else {
// it's not a custom app as it has no zim file set
self = .kiwix
return
}
guard let zimURL: URL = Bundle.main.url(forResource: zimFileName, withExtension: "zim") else {
fatalError("zim file named: \(zimFileName) cannot be found")
}
self = .custom(zimFileURL: zimURL)
}
}
enum Brand {
static let appName: String = Config.value(for: .displayName) ?? "Kiwix"
static let appStoreId: String = Config.value(for: .appStoreID) ?? "id997079563"
static let welcomeLogoImageName: String = "welcomeLogo"
static var defaultExternalLinkPolicy: ExternalLinkLoadingPolicy {
guard let policyString: String = Config.value(for: .externalLinkDefaultPolicy),
let policy = ExternalLinkLoadingPolicy(rawValue: policyString) else {
return .alwaysAsk
}
return policy
}
static var defaultSearchSnippetMode: SearchResultSnippetMode {
guard FeatureFlags.showSearchSnippetInSettings else {
// for custom apps, where we do not show this in settings, it should be disabled by default
return .disabled
}
return .firstSentence
}
}
enum Config: String {
case appStoreID = "APP_STORE_ID"
case displayName = "CFBundleDisplayName"
// this marks if the app is custom or not
case customZimFile = "CUSTOM_ZIM_FILE"
case showExternalLinkSettings = "SETTINGS_SHOW_EXTERNAL_LINK_OPTION"
case externalLinkDefaultPolicy = "SETTINGS_DEFAULT_EXTERNAL_LINK_TO"
case showSearchSnippetInSettings = "SETTINGS_SHOW_SEARCH_SNIPPET"
static func value<T>(for key: Config) -> T? where T: LosslessStringConvertible {
guard let object = Bundle.main.object(forInfoDictionaryKey: key.rawValue) else {
os_log("Missing key from bundle: %@", log: Log.Branding, type: .error, key.rawValue)
return nil
}
switch object {
case let value as T:
return value
case let string as String:
guard let value = T(string) else { fallthrough }
return value
default:
os_log("Invalid value type found for key: %@", log: Log.Branding, type: .error, key.rawValue)
return nil
}
}
}

View File

@ -8,20 +8,18 @@
import Foundation import Foundation
struct FeatureFlags { enum FeatureFlags {
static var wikipediaDarkUserCSS: Bool { #if DEBUG
#if DEBUG static let wikipediaDarkUserCSS: Bool = true
true static let map: Bool = true
#else #else
false static let wikipediaDarkUserCSS: Bool = false
#endif static let map: Bool = false
} #endif
/// Custom apps, which have a bundled zim file, do not require library access
static var map: Bool { /// this will remove all library related features
#if DEBUG static let hasLibrary: Bool = !AppType.isCustom
true
#else static let showExternalLinkOptionInSettings: Bool = Config.value(for: .showExternalLinkSettings) ?? true
false static let showSearchSnippetInSettings: Bool = Config.value(for: .showSearchSnippetInSettings) ?? true
#endif
}
} }

View File

@ -17,4 +17,5 @@ struct Log {
static let LibraryOperations = OSLog(subsystem: subsystem, category: "LibraryOperations") static let LibraryOperations = OSLog(subsystem: subsystem, category: "LibraryOperations")
static let OPDS = OSLog(subsystem: subsystem, category: "OPDS") static let OPDS = OSLog(subsystem: subsystem, category: "OPDS")
static let URLSchemeHandler = OSLog(subsystem: subsystem, category: "URLSchemeHandler") static let URLSchemeHandler = OSLog(subsystem: subsystem, category: "URLSchemeHandler")
static let Branding = OSLog(subsystem: subsystem, category: "Branding")
} }

View File

@ -5,6 +5,7 @@
// Created by Chris Li on 11/6/21. // Created by Chris Li on 11/6/21.
// Copyright © 2021 Chris Li. All rights reserved. // Copyright © 2021 Chris Li. All rights reserved.
// //
import Foundation import Foundation
extension URL { extension URL {
@ -22,5 +23,15 @@ extension URL {
["http", "https"].contains(scheme) ["http", "https"].contains(scheme)
} }
static let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) // swiftlint:disable:next force_try
static let documentDirectory = try! FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
)
init(appStoreReviewForName appName: String, appStoreID: String) {
self.init(string: "itms-apps://itunes.apple.com/us/app/\(appName)/\(appStoreID)?action=write-review")!
}
} }

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -2,8 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key></key> <key>APP_STORE_ID</key>
<string></string> <string>$(APP_STORE_ID)</string>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>org.kiwix.library_refresh</string> <string>org.kiwix.library_refresh</string>

View File

@ -201,3 +201,4 @@ The software as well as the content is free to use for anyone.";
"First Paragraph" = "First Paragraph"; "First Paragraph" = "First Paragraph";
"First Sentence" = "First Sentence"; "First Sentence" = "First Sentence";
"Matches" = "Matches"; "Matches" = "Matches";
"Loading..." = "Loading...";

5
Support/kiwix.xcconfig Normal file
View File

@ -0,0 +1,5 @@
// Copyright © 2023 Kiwix.
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
APP_STORE_ID = id997079563

View File

@ -13,10 +13,10 @@ extension Defaults.Keys {
static let webViewTextSizeAdjustFactor = Key<Double>("webViewZoomScale", default: 1) static let webViewTextSizeAdjustFactor = Key<Double>("webViewZoomScale", default: 1)
static let webViewPageZoom = Key<Double>("webViewPageZoom", default: 1) static let webViewPageZoom = Key<Double>("webViewPageZoom", default: 1)
static let externalLinkLoadingPolicy = Key<ExternalLinkLoadingPolicy>( static let externalLinkLoadingPolicy = Key<ExternalLinkLoadingPolicy>(
"externalLinkLoadingPolicy", default: .alwaysAsk "externalLinkLoadingPolicy", default: Brand.defaultExternalLinkPolicy
) )
static let searchResultSnippetMode = Key<SearchResultSnippetMode>( static let searchResultSnippetMode = Key<SearchResultSnippetMode>(
"searchResultSnippetMode", default: .firstSentence "searchResultSnippetMode", default: Brand.defaultSearchSnippetMode
) )
// //
// // UI // // UI

View File

@ -24,7 +24,6 @@ class DirectoryMonitor {
init(url: URL, onChange: ((URL) -> Void)? = nil) { init(url: URL, onChange: ((URL) -> Void)? = nil) {
self.url = url self.url = url
self.onChange = onChange self.onChange = onChange
start()
} }
// MARK: Monitoring // MARK: Monitoring

View File

@ -177,12 +177,15 @@ enum LibraryTabItem: String, CaseIterable, Identifiable {
enum NavigationItem: Hashable, Identifiable { enum NavigationItem: Hashable, Identifiable {
var id: Int { hashValue } var id: Int { hashValue }
case loading
case reading, bookmarks, map(location: CLLocation?), tab(objectID: NSManagedObjectID) case reading, bookmarks, map(location: CLLocation?), tab(objectID: NSManagedObjectID)
case opened, categories, new, downloads case opened, categories, new, downloads
case settings case settings
var name: String { var name: String {
switch self { switch self {
case .loading:
return "Loading"
case .reading: case .reading:
return "Reading" return "Reading"
case .bookmarks: case .bookmarks:
@ -206,6 +209,8 @@ enum NavigationItem: Hashable, Identifiable {
var icon: String { var icon: String {
switch self { switch self {
case .loading:
return "loading"
case .reading: case .reading:
return "book" return "book"
case .bookmarks: case .bookmarks:
@ -228,7 +233,6 @@ enum NavigationItem: Hashable, Identifiable {
} }
} }
enum SearchResultSnippetMode: String, CaseIterable, Identifiable, Defaults.Serializable { enum SearchResultSnippetMode: String, CaseIterable, Identifiable, Defaults.Serializable {
case disabled, firstParagraph, firstSentence, matches case disabled, firstParagraph, firstSentence, matches

View File

@ -6,7 +6,9 @@
// Copyright © 2022 Chris Li. All rights reserved. // Copyright © 2022 Chris Li. All rights reserved.
// //
class Formatter { import Foundation
enum Formatter {
static let dateShort: DateFormatter = { static let dateShort: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateStyle = .short formatter.dateStyle = .short
@ -45,7 +47,7 @@ class Formatter {
guard abs >= 1000 else {return "\(sign)\(abs)"} guard abs >= 1000 else {return "\(sign)\(abs)"}
let exp = Int(log10(Double(abs)) / log10(1000)) let exp = Int(log10(Double(abs)) / log10(1000))
let units = ["K", "M", "G", "T", "P", "E"] let units = ["K", "M", "G", "T", "P", "E"]
let rounded = round(10 * Double(abs) / pow(1000.0,Double(exp))) / 10; let rounded = round(10 * Double(abs) / pow(1000.0, Double(exp))) / 10
return "\(sign)\(rounded)\(units[exp-1])" return "\(sign)\(rounded)\(units[exp-1])"
} }
} }

View File

@ -24,7 +24,7 @@ struct LibraryOperations {
/// Open a zim file with url /// Open a zim file with url
/// - Parameter url: url of the zim file /// - Parameter url: url of the zim file
@discardableResult @discardableResult
static func open(url: URL) -> ZimFileMetaData? { static func open(url: URL, onComplete: (() -> Void)? = nil) -> ZimFileMetaData? {
guard let metadata = ZimFileService.getMetaData(url: url), guard let metadata = ZimFileService.getMetaData(url: url),
let fileURLBookmark = ZimFileService.getBookmarkData(url: url) else { return nil } let fileURLBookmark = ZimFileService.getBookmarkData(url: url) else { return nil }
@ -45,18 +45,24 @@ struct LibraryOperations {
zimFile.fileURLBookmark = fileURLBookmark zimFile.fileURLBookmark = fileURLBookmark
zimFile.isMissing = false zimFile.isMissing = false
if context.hasChanges { try? context.save() } if context.hasChanges { try? context.save() }
DispatchQueue.main.async {
onComplete?()
}
} }
return metadata return metadata
} }
/// Reopen zim files from url bookmark data. /// Reopen zim files from url bookmark data.
static func reopen() { static func reopen(onComplete: (() -> Void)?) {
var successCount = 0 var successCount = 0
let context = Database.shared.container.viewContext let context = Database.shared.container.viewContext
let request = ZimFile.fetchRequest(predicate: ZimFile.withFileURLBookmarkPredicate) let request = ZimFile.fetchRequest(predicate: ZimFile.withFileURLBookmarkPredicate)
guard let zimFiles = try? context.fetch(request) else { return } guard let zimFiles = try? context.fetch(request) else {
onComplete?()
return
}
zimFiles.forEach { zimFile in zimFiles.forEach { zimFile in
guard let data = zimFile.fileURLBookmark else { return } guard let data = zimFile.fileURLBookmark else { return }
do { do {
@ -77,6 +83,7 @@ struct LibraryOperations {
} }
os_log("Reopened %d out of %d zim files", log: Log.LibraryOperations, type: .info, successCount, zimFiles.count) os_log("Reopened %d out of %d zim files", log: Log.LibraryOperations, type: .info, successCount, zimFiles.count)
onComplete?()
} }
/// Scan a directory and open available zim files inside it /// Scan a directory and open available zim files inside it

View File

@ -13,6 +13,7 @@ import WebKit
import Defaults import Defaults
import OrderedCollections import OrderedCollections
import CoreKiwix
// swiftlint:disable file_length // swiftlint:disable file_length
// swiftlint:disable:next type_body_length // swiftlint:disable:next type_body_length
@ -46,7 +47,17 @@ final class BrowserViewModel: NSObject, ObservableObject,
@Published private(set) var articleBookmarked = false @Published private(set) var articleBookmarked = false
@Published private(set) var outlineItems = [OutlineItem]() @Published private(set) var outlineItems = [OutlineItem]()
@Published private(set) var outlineItemTree = [OutlineItem]() @Published private(set) var outlineItemTree = [OutlineItem]()
@Published private(set) var url: URL? @Published private(set) var url: URL? {
didSet {
if !FeatureFlags.hasLibrary, url == nil {
loadMainArticle()
}
if url != oldValue {
bookmarkFetchedResultsController.fetchRequest.predicate = Self.bookmarksPredicateFor(url: url)
try? bookmarkFetchedResultsController.performFetch()
}
}
}
@Published var externalURL: URL? @Published var externalURL: URL?
private(set) var tabID: NSManagedObjectID? { private(set) var tabID: NSManagedObjectID? {
@ -67,18 +78,26 @@ final class BrowserViewModel: NSObject, ObservableObject,
private var canGoBackObserver: NSKeyValueObservation? private var canGoBackObserver: NSKeyValueObservation?
private var canGoForwardObserver: NSKeyValueObservation? private var canGoForwardObserver: NSKeyValueObservation?
private var titleURLObserver: AnyCancellable? private var titleURLObserver: AnyCancellable?
private var bookmarkFetchedResultsController: NSFetchedResultsController<Bookmark>? private let bookmarkFetchedResultsController: NSFetchedResultsController<Bookmark>
/// A temporary placeholder for the url that should be opened in a new tab, set on macOS only /// A temporary placeholder for the url that should be opened in a new tab, set on macOS only
static var urlForNewTab: URL? static var urlForNewTab: URL?
private var cancellables: Set<AnyCancellable> = []
// MARK: - Lifecycle // MARK: - Lifecycle
init(tabID: NSManagedObjectID? = nil) { init(tabID: NSManagedObjectID? = nil) {
self.tabID = tabID self.tabID = tabID
webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) webView = WKWebView(frame: .zero, configuration: WebViewConfiguration())
// Bookmark fetching:
bookmarkFetchedResultsController = NSFetchedResultsController(
fetchRequest: Bookmark.fetchRequest(), // initially empty
managedObjectContext: Database.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
super.init() super.init()
bookmarkFetchedResultsController.delegate = self
// configure web view // configure web view
webView.allowsBackForwardNavigationGestures = true webView.allowsBackForwardNavigationGestures = true
webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile // for font adjustment to work webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile // for font adjustment to work
@ -116,7 +135,6 @@ final class BrowserViewModel: NSObject, ObservableObject,
guard let title, let url else { return } guard let title, let url else { return }
self?.didUpdate(title: title, url: url) self?.didUpdate(title: title, url: url)
} }
bookmarkFetchedResultsController?.delegate = self
} }
private func didUpdate(title: String, url: URL) { private func didUpdate(title: String, url: URL) {
@ -138,17 +156,6 @@ final class BrowserViewModel: NSObject, ObservableObject,
tab.title = title tab.title = title
tab.zimFile = zimFile tab.zimFile = zimFile
} }
// setup bookmark fetched results controller
bookmarkFetchedResultsController = NSFetchedResultsController(
fetchRequest: Bookmark.fetchRequest(predicate: {
return NSPredicate(format: "articleURL == %@", url as CVarArg)
}()),
managedObjectContext: Database.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
try? bookmarkFetchedResultsController?.performFetch()
} }
func updateLastOpened() { func updateLastOpened() {
@ -170,6 +177,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
func load(url: URL) { func load(url: URL) {
guard webView.url != url else { return } guard webView.url != url else { return }
webView.load(URLRequest(url: url)) webView.load(URLRequest(url: url))
self.url = url
} }
func loadRandomArticle(zimFileID: UUID? = nil) { func loadRandomArticle(zimFileID: UUID? = nil) {
@ -312,9 +320,10 @@ final class BrowserViewModel: NSObject, ObservableObject,
let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration())
webView.load(URLRequest(url: url)) webView.load(URLRequest(url: url))
return WebViewController(webView: webView) return WebViewController(webView: webView)
}, actionProvider: { _ in },
actionProvider: { _ in
var actions = [UIAction]() var actions = [UIAction]()
// open url // open url
actions.append( actions.append(
UIAction(title: "Open".localized, image: UIImage(systemName: "doc.text")) { _ in UIAction(title: "Open".localized, image: UIImage(systemName: "doc.text")) { _ in
@ -326,19 +335,23 @@ final class BrowserViewModel: NSObject, ObservableObject,
NotificationCenter.openURL(url, inNewTab: true) NotificationCenter.openURL(url, inNewTab: true)
} }
) )
// bookmark // bookmark
let bookmarkAction: UIAction = { let bookmarkAction: UIAction = {
let context = Database.viewContext let context = Database.viewContext
let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg) let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg)
let request = Bookmark.fetchRequest(predicate: predicate) let request = Bookmark.fetchRequest(predicate: predicate)
if let bookmarks = try? context.fetch(request), !bookmarks.isEmpty { if let bookmarks = try? context.fetch(request),
!bookmarks.isEmpty {
return UIAction(title: "Remove Bookmark".localized, return UIAction(title: "Remove Bookmark".localized,
image: UIImage(systemName: "star.slash.fill")) { [weak self] _ in image: UIImage(systemName: "star.slash.fill")) { [weak self] _ in
self?.deleteBookmark(url: url) self?.deleteBookmark(url: url)
} }
} else { } else {
return UIAction(title: "Bookmark".localized, image: UIImage(systemName: "star")) { [weak self] _ in return UIAction(
title: "Bookmark".localized,
image: UIImage(systemName: "star")
) { [weak self] _ in
self?.createBookmark(url: url) self?.createBookmark(url: url)
} }
} }
@ -531,4 +544,9 @@ final class BrowserViewModel: NSObject, ObservableObject,
} }
} }
} }
private static func bookmarksPredicateFor(url: URL?) -> NSPredicate? {
guard let url else { return nil }
return NSPredicate(format: "articleURL == %@", url as CVarArg)
}
} }

View File

@ -11,17 +11,9 @@ import WebKit
@MainActor @MainActor
class NavigationViewModel: ObservableObject { class NavigationViewModel: ObservableObject {
@Published var currentItem: NavigationItem? // remained optional due to focusedSceneValue conformance
@Published var readingURL: URL? @Published var currentItem: NavigationItem? = .loading
init() {
#if os(macOS)
currentItem = .reading
#elseif os(iOS)
navigateToMostRecentTab()
#endif
}
// MARK: - Tab Management // MARK: - Tab Management
private func makeTab(context: NSManagedObjectContext) -> Tab { private func makeTab(context: NSManagedObjectContext) -> Tab {
@ -55,11 +47,9 @@ class NavigationViewModel: ObservableObject {
@MainActor @MainActor
func tabIDFor(url: URL?) -> NSManagedObjectID { func tabIDFor(url: URL?) -> NSManagedObjectID {
guard let url else { guard let url,
return createTab() let coordinator = Database.viewContext.persistentStoreCoordinator,
} let tabID = coordinator.managedObjectID(forURIRepresentation: url) else {
let coordinator = Database.viewContext.persistentStoreCoordinator
guard let tabID = coordinator?.managedObjectID(forURIRepresentation: url) else {
return createTab() return createTab()
} }
return tabID return tabID

View File

@ -42,7 +42,7 @@ struct BrowserTab: View {
.searchable(text: $search.searchText, placement: .toolbar) .searchable(text: $search.searchText, placement: .toolbar)
.modify { view in .modify { view in
#if os(macOS) #if os(macOS)
view.navigationTitle(browser.articleTitle.isEmpty ? "Kiwix" : browser.articleTitle) view.navigationTitle(browser.articleTitle.isEmpty ? Brand.appName : browser.articleTitle)
.navigationSubtitle(browser.zimFileName) .navigationSubtitle(browser.zimFileName)
#elseif os(iOS) #elseif os(iOS)
view view
@ -59,7 +59,7 @@ struct BrowserTab: View {
struct Content: View { struct Content: View {
@Environment(\.isSearching) private var isSearching @Environment(\.isSearching) private var isSearching
@EnvironmentObject private var browser: BrowserViewModel @EnvironmentObject private var browser: BrowserViewModel
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
Group { Group {
@ -70,7 +70,7 @@ struct BrowserTab: View {
#elseif os(iOS) #elseif os(iOS)
.environment(\.horizontalSizeClass, proxy.size.width > 750 ? .regular : .compact) .environment(\.horizontalSizeClass, proxy.size.width > 750 ? .regular : .compact)
#endif #endif
} else if browser.url == nil { } else if browser.url == nil && FeatureFlags.hasLibrary {
Welcome() Welcome()
} else { } else {
WebView().ignoresSafeArea() WebView().ignoresSafeArea()

View File

@ -0,0 +1,13 @@
// Copyright © 2023 Kiwix.
import SwiftUI
struct LoadingView: View {
var body: some View {
Text("Loading...".localized)
}
}
#Preview {
LoadingView()
}

View File

@ -11,7 +11,7 @@ import SwiftUI
struct BookmarkButton: View { struct BookmarkButton: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@EnvironmentObject private var browser: BrowserViewModel @EnvironmentObject private var browser: BrowserViewModel
@State private var isShowingBookmark = false @State private var isShowingPopOver = false
var body: some View { var body: some View {
#if os(macOS) #if os(macOS)
@ -45,7 +45,7 @@ struct BookmarkButton: View {
} }
} }
Button { Button {
isShowingBookmark = true isShowingPopOver = true
} label: { } label: {
Label("Show Bookmarks".localized, systemImage: "list.star") Label("Show Bookmarks".localized, systemImage: "list.star")
} }
@ -57,15 +57,15 @@ struct BookmarkButton: View {
.renderingMode(browser.articleBookmarked ? .original : .template) .renderingMode(browser.articleBookmarked ? .original : .template)
} }
} primaryAction: { } primaryAction: {
isShowingBookmark = true isShowingPopOver = true
} }
.help("Show bookmarks. Long press to bookmark or unbookmark the current article.".localized) .help("Show bookmarks. Long press to bookmark or unbookmark the current article.".localized)
.popover(isPresented: $isShowingBookmark) { .popover(isPresented: $isShowingPopOver) {
NavigationView { NavigationView {
Bookmarks().navigationBarTitleDisplayMode(.inline).toolbar { Bookmarks().navigationBarTitleDisplayMode(.inline).toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button { Button {
isShowingBookmark = false isShowingPopOver = false
} label: { } label: {
Text("Done".localized).fontWeight(.semibold) Text("Done".localized).fontWeight(.semibold)
} }

View File

@ -51,10 +51,12 @@ struct TabsManagerButton: View {
} }
} }
Section { Section {
Button { if FeatureFlags.hasLibrary {
presentedSheet = .library Button {
} label: { presentedSheet = .library
Label("Library".localized, systemImage: "folder") } label: {
Label("Library".localized, systemImage: "folder")
}
} }
Button { Button {
presentedSheet = .settings presentedSheet = .settings

View File

@ -86,8 +86,10 @@ struct SidebarNavigationCommands: View {
var body: some View { var body: some View {
buildButtons([.reading, .bookmarks], modifiers: [.command]) buildButtons([.reading, .bookmarks], modifiers: [.command])
Divider() if FeatureFlags.hasLibrary {
buildButtons([.opened, .categories, .downloads, .new], modifiers: [.command, .control]) Divider()
buildButtons([.opened, .categories, .downloads, .new], modifiers: [.command, .control])
}
} }
private func buildButtons(_ navigationItems: [NavigationItem], modifiers: EventModifiers = []) -> some View { private func buildButtons(_ navigationItems: [NavigationItem], modifiers: EventModifiers = []) -> some View {

View File

@ -108,19 +108,21 @@ struct SearchResults: View {
} }
} header: { recentSearchHeader } } header: { recentSearchHeader }
} }
Section { if FeatureFlags.hasLibrary {
ForEach(zimFiles) { zimFile in Section {
HStack { ForEach(zimFiles) { zimFile in
Toggle(zimFile.name, isOn: Binding<Bool>(get: { HStack {
zimFile.includedInSearch Toggle(zimFile.name, isOn: Binding<Bool>(get: {
}, set: { zimFile.includedInSearch
zimFile.includedInSearch = $0 }, set: {
try? managedObjectContext.save() zimFile.includedInSearch = $0
})) try? managedObjectContext.save()
Spacer() }))
Spacer()
}
} }
} } header: { searchFilterHeader }
} header: { searchFilterHeader } }
} }
} }

View File

@ -25,19 +25,23 @@ struct ReadingSettings: View {
Button("Reset".localized) { webViewPageZoom = 1 }.disabled(webViewPageZoom == 1) Button("Reset".localized) { webViewPageZoom = 1 }.disabled(webViewPageZoom == 1)
} }
} }
SettingSection(name: "External link".localized) { if FeatureFlags.showExternalLinkOptionInSettings {
Picker(selection: $externalLinkLoadingPolicy) { SettingSection(name: "External link".localized) {
ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in Picker(selection: $externalLinkLoadingPolicy) {
Text(loadingPolicy.name.localized).tag(loadingPolicy) ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in
} Text(loadingPolicy.name.localized).tag(loadingPolicy)
} label: { } }
} label: { }
}
} }
SettingSection(name: "Search snippet".localized) { if FeatureFlags.showSearchSnippetInSettings {
Picker(selection: $searchResultSnippetMode) { SettingSection(name: "Search snippet".localized) {
ForEach(SearchResultSnippetMode.allCases) { snippetMode in Picker(selection: $searchResultSnippetMode) {
Text(snippetMode.name.localized).tag(snippetMode) ForEach(SearchResultSnippetMode.allCases) { snippetMode in
} Text(snippetMode.name.localized).tag(snippetMode)
} label: { } }
} label: { }
}
} }
} }
.padding() .padding()
@ -118,15 +122,24 @@ struct Settings: View {
} }
var body: some View { var body: some View {
List { if FeatureFlags.hasLibrary {
readingSettings List {
librarySettings readingSettings
catalogSettings librarySettings
backupSettings catalogSettings
miscellaneous backupSettings
miscellaneous
}
.modifier(ToolbarRoleBrowser())
.navigationTitle("Settings".localized)
} else {
List {
readingSettings
miscellaneous
}
.modifier(ToolbarRoleBrowser())
.navigationTitle("Settings".localized)
} }
.modifier(ToolbarRoleBrowser())
.navigationTitle("Settings".localized)
} }
var readingSettings: some View { var readingSettings: some View {
@ -135,14 +148,18 @@ struct Settings: View {
Text("Page zoom".localized + Text("Page zoom".localized +
": \(Formatter.percent.string(from: NSNumber(value: webViewPageZoom)) ?? "")") ": \(Formatter.percent.string(from: NSNumber(value: webViewPageZoom)) ?? "")")
} }
Picker("External link".localized, selection: $externalLinkLoadingPolicy) { if FeatureFlags.showExternalLinkOptionInSettings {
ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in Picker("External link".localized, selection: $externalLinkLoadingPolicy) {
Text(loadingPolicy.name.localized).tag(loadingPolicy) ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in
Text(loadingPolicy.name.localized).tag(loadingPolicy)
}
} }
} }
Picker("Search snippet".localized, selection: $searchResultSnippetMode) { if FeatureFlags.showSearchSnippetInSettings {
ForEach(SearchResultSnippetMode.allCases) { snippetMode in Picker("Search snippet".localized, selection: $searchResultSnippetMode) {
Text(snippetMode.name.localized).tag(snippetMode) ForEach(SearchResultSnippetMode.allCases) { snippetMode in
Text(snippetMode.name.localized).tag(snippetMode)
}
} }
} }
} }
@ -203,7 +220,8 @@ struct Settings: View {
Section("Misc".localized) { Section("Misc".localized) {
Button("Feedback".localized) { UIApplication.shared.open(URL(string: "mailto:feedback@kiwix.org")!) } Button("Feedback".localized) { UIApplication.shared.open(URL(string: "mailto:feedback@kiwix.org")!) }
Button("Rate the App".localized) { Button("Rate the App".localized) {
let url = URL(string: "itms-apps://itunes.apple.com/us/app/kiwix/id997079563?action=write-review")! let url = URL(appStoreReviewForName: Brand.appName.lowercased(),
appStoreID: Brand.appStoreId)
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
NavigationLink("About".localized) { About() } NavigationLink("About".localized) { About() }

View File

@ -94,15 +94,15 @@ struct Welcome: View {
} }
} }
/// Kiwix logo shown in onboarding view /// App logo shown in onboarding view
private var logo: some View { private var logo: some View {
VStack(spacing: 6) { VStack(spacing: 6) {
Image("Kiwix_logo_v3") Image(Brand.welcomeLogoImageName)
.resizable() .resizable()
.aspectRatio(1, contentMode: .fit) .aspectRatio(1, contentMode: .fit)
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
.background(RoundedRectangle(cornerRadius: 10, style: .continuous).foregroundColor(.white)) .background(RoundedRectangle(cornerRadius: 10, style: .continuous).foregroundColor(.white))
Text("KIWIX").font(.largeTitle).fontWeight(.bold) Text(Brand.appName.uppercased()).font(.largeTitle).fontWeight(.bold)
} }
} }