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
// Kiwix
//
@ -21,13 +20,8 @@ struct Kiwix: App {
init() {
fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) }
UNUserNotificationCenter.current().delegate = appDelegate
LibraryOperations.reopen()
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
LibraryOperations.registerBackgroundTask()
LibraryOperations.applyLibraryAutoRefreshSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
UNUserNotificationCenter.current().delegate = appDelegate
}
var body: some Scene {
@ -50,6 +44,23 @@ struct Kiwix: App {
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 {
CommandGroup(replacing: .undoRedo) {

View File

@ -10,6 +10,7 @@ import SwiftUI
import UserNotifications
import Combine
import Defaults
import CoreKiwix
#if os(macOS)
@main
@ -19,10 +20,6 @@ struct Kiwix: App {
init() {
UNUserNotificationCenter.current().delegate = notificationCenterDelegate
LibraryOperations.reopen()
LibraryOperations.scanDirectory(URL.documentDirectory)
LibraryOperations.applyFileBackupSetting()
DownloadService.shared.restartHeartbeatIfNeeded()
}
var body: some Scene {
@ -57,11 +54,13 @@ struct Kiwix: App {
Settings {
TabView {
ReadingSettings()
LibrarySettings()
if FeatureFlags.hasLibrary {
LibrarySettings()
.environmentObject(libraryRefreshViewModel)
}
About()
}
.frame(width: 550, height: 400)
.environmentObject(libraryRefreshViewModel)
}
}
@ -95,9 +94,11 @@ struct RootView: View {
ForEach(primaryItems, id: \.self) { navigationItem in
Label(navigationItem.name.localized, systemImage: navigationItem.icon)
}
Section("Library".localized) {
ForEach(libraryItems, id: \.self) { navigationItem in
Label(navigationItem.name.localized, systemImage: navigationItem.icon)
if FeatureFlags.hasLibrary {
Section("Library".localized) {
ForEach(libraryItems, id: \.self) { navigationItem in
Label(navigationItem.name.localized, systemImage: navigationItem.icon)
}
}
}
}
@ -111,6 +112,8 @@ struct RootView: View {
}.help("Show sidebar".localized)
}
switch navigation.currentItem {
case .loading:
LoadingView()
case .reading:
BrowserTab().environmentObject(browser)
.withHostingWindow { window in
@ -152,6 +155,20 @@ struct RootView: View {
}
.onReceive(appTerminates) { _ in
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()
return apperance
}()
searchController.searchBar.autocorrectionType = .no
navigationItem.titleView = searchController.searchBar
searchController.automaticallyShowsCancelButton = false
searchController.delegate = self
@ -54,10 +55,6 @@ class CompactViewController: UIHostingController<AnyView>, UISearchControllerDel
guard self?.searchController.searchBar.text != searchText else { return }
self?.searchController.searchBar.text = searchText
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
openURLObserver = NotificationCenter.default.addObserver(
forName: .openURL, object: nil, queue: nil
) { [weak self] _ in
@ -65,12 +62,11 @@ class CompactViewController: UIHostingController<AnyView>, UISearchControllerDel
self?.navigationItem.setRightBarButton(nil, animated: true)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
deinit {
NotificationCenter.default.removeObserver(self)
}
func willPresentSearchController(_ searchController: UISearchController) {
navigationController?.setToolbarHidden(true, animated: true)
navigationItem.setRightBarButton(

View File

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

View File

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

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
@ -44,7 +44,6 @@
973A0DE8281DD7EB00B41E71 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A7E224567A5A00F6F6FF /* Log.swift */; };
973A0DEB281DDBB600B41E71 /* ZimFilesDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973A0DE9281DDBB600B41E71 /* ZimFilesDownloads.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 */; };
973A0E032831057200B41E71 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B3BACD2736CE3500A23F49 /* URL.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 */; };
97DE2BA6283A944100C63D9B /* GridCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DE2BA4283A944100C63D9B /* GridCommon.swift */; };
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 */; };
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; };
/* End PBXBuildFile section */
@ -234,13 +237,16 @@
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>"; };
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>"; };
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; };
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@ -317,6 +323,7 @@
974E7EE22930201500BDF59C /* ZimFileService */,
9735D0B82775363900C7D495 /* DataModel.xcdatamodeld */,
973A0DE5281DC8F400B41E71 /* DownloadService.swift */,
980179502B0DF42100E8E1E2 /* Brand.swift */,
);
path = Model;
sourceTree = "<group>";
@ -407,6 +414,7 @@
975088BF287EEE2900273181 /* BuildingBlocks */ = {
isa = PBXGroup;
children = (
983D22662B16ABB6005EBAF1 /* LoadingView.swift */,
97486D05284A36790096E4DD /* ArticleCell.swift */,
972096E62AE421C300B378B0 /* Attribute.swift */,
97341C6C2852248500BC273E /* DownloadTaskCell.swift */,
@ -553,7 +561,7 @@
97E94B26271EF359005B0295 /* Support */ = {
isa = PBXGroup;
children = (
97E94B1D271EF250005B0295 /* Assets.xcassets */,
983A3F552B129F6B000E7A51 /* Assets.xcassets */,
9713F7782AE416E5007DD9EC /* CoreKiwix.modulemap */,
97B707042974637200562392 /* Info.plist */,
9735B88B279D9A74005F0D1A /* injection.js */,
@ -561,6 +569,7 @@
9779A5D02456796A00F6F6FF /* Kiwix-Bridging-Header.h */,
970885D0271339A300C5795C /* wikipedia_dark.css */,
8E4396492B02E455007F0BC4 /* Localizable.strings */,
980179532B0E402C00E8E1E2 /* kiwix.xcconfig */,
);
path = Support;
sourceTree = "<group>";
@ -638,7 +647,7 @@
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1500;
ORGANIZATIONNAME = "Chris Li";
ORGANIZATIONNAME = Kiwix;
TargetAttributes = {
97008AB92974A5BF0076E60C = {
CreatedOnToolsVersion = 14.2;
@ -693,9 +702,10 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
973A0DF72830929C00B41E71 /* Assets.xcassets in Resources */,
980179542B0E402C00E8E1E2 /* kiwix.xcconfig in Resources */,
8E4396462B02E455007F0BC4 /* Localizable.strings in Resources */,
979D3A7C284159BF00E396B8 /* injection.js in Resources */,
983A3F562B129F6B000E7A51 /* Assets.xcassets in Resources */,
97DE2BAD283B133700C63D9B /* wikipedia_dark.css in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -724,6 +734,7 @@
);
inputPaths = (
);
name = "Run Script";
outputFileListPaths = (
);
outputPaths = (
@ -820,6 +831,7 @@
972DE4B42814A4F6004FD9B9 /* DataModel.xcdatamodeld in Sources */,
976BAEBD2849056B0049404F /* SearchOperation.swift in Sources */,
976D90E428159BFA00CC7D29 /* Favicon.swift in Sources */,
983D22672B16ABB6005EBAF1 /* LoadingView.swift in Sources */,
976D90DB281584BF00CC7D29 /* FlavorTag.swift in Sources */,
972DE4BD2814A5BE004FD9B9 /* OPDSParser.mm in Sources */,
972DE4B52814A502004FD9B9 /* Entities.swift in Sources */,
@ -841,6 +853,7 @@
973A0DE7281DC8F400B41E71 /* DownloadService.swift in Sources */,
9721BBB72841C16D005C910D /* Message.swift in Sources */,
97BD6C9D2886E06B004A8532 /* HTMLParser.swift in Sources */,
980179512B0DF42100E8E1E2 /* Brand.swift in Sources */,
97677B572A8FA80000F523AB /* FileImport.swift in Sources */,
972727AA2A89122F00BCAF75 /* App_macOS.swift in Sources */,
97DE2BA3283A8E5C00C63D9B /* LibraryViewModel.swift in Sources */,
@ -961,6 +974,7 @@
};
972DE4B12814A3D9004FD9B9 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 980179532B0E402C00E8E1E2 /* kiwix.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
@ -970,13 +984,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Support/Kiwix.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 119;
CURRENT_PROJECT_VERSION = 120;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = L7HWM3SP3L;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Support/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Kiwix;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Kiwix needs permission to saves images to your photos app.";
@ -989,7 +1004,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.2;
MARKETING_VERSION = 3.3;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.Kiwix;
@ -1007,6 +1022,7 @@
};
972DE4B22814A3D9004FD9B9 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 980179532B0E402C00E8E1E2 /* kiwix.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
@ -1016,18 +1032,20 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = Support/Kiwix.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 119;
CURRENT_PROJECT_VERSION = 120;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = L7HWM3SP3L;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Support/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Kiwix;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Kiwix needs permission to saves images to your photos app.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchKiwix.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@ -1035,7 +1053,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.2;
MARKETING_VERSION = 3.3;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = self.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
struct FeatureFlags {
static var wikipediaDarkUserCSS: Bool {
#if DEBUG
true
#else
false
#endif
}
static var map: Bool {
#if DEBUG
true
#else
false
#endif
}
enum FeatureFlags {
#if DEBUG
static let wikipediaDarkUserCSS: Bool = true
static let map: Bool = true
#else
static let wikipediaDarkUserCSS: Bool = false
static let map: Bool = false
#endif
/// Custom apps, which have a bundled zim file, do not require library access
/// this will remove all library related features
static let hasLibrary: Bool = !AppType.isCustom
static let showExternalLinkOptionInSettings: Bool = Config.value(for: .showExternalLinkSettings) ?? true
static let showSearchSnippetInSettings: Bool = Config.value(for: .showSearchSnippetInSettings) ?? true
}

View File

@ -17,4 +17,5 @@ struct Log {
static let LibraryOperations = OSLog(subsystem: subsystem, category: "LibraryOperations")
static let OPDS = OSLog(subsystem: subsystem, category: "OPDS")
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.
// Copyright © 2021 Chris Li. All rights reserved.
//
import Foundation
extension URL {
@ -22,5 +23,15 @@ extension URL {
["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">
<plist version="1.0">
<dict>
<key></key>
<string></string>
<key>APP_STORE_ID</key>
<string>$(APP_STORE_ID)</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<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 Sentence" = "First Sentence";
"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 webViewPageZoom = Key<Double>("webViewPageZoom", default: 1)
static let externalLinkLoadingPolicy = Key<ExternalLinkLoadingPolicy>(
"externalLinkLoadingPolicy", default: .alwaysAsk
"externalLinkLoadingPolicy", default: Brand.defaultExternalLinkPolicy
)
static let searchResultSnippetMode = Key<SearchResultSnippetMode>(
"searchResultSnippetMode", default: .firstSentence
"searchResultSnippetMode", default: Brand.defaultSearchSnippetMode
)
//
// // UI

View File

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

View File

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

View File

@ -6,7 +6,9 @@
// Copyright © 2022 Chris Li. All rights reserved.
//
class Formatter {
import Foundation
enum Formatter {
static let dateShort: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
@ -45,7 +47,7 @@ class Formatter {
guard abs >= 1000 else {return "\(sign)\(abs)"}
let exp = Int(log10(Double(abs)) / log10(1000))
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])"
}
}

View File

@ -24,7 +24,7 @@ struct LibraryOperations {
/// Open a zim file with url
/// - Parameter url: url of the zim file
@discardableResult
static func open(url: URL) -> ZimFileMetaData? {
static func open(url: URL, onComplete: (() -> Void)? = nil) -> ZimFileMetaData? {
guard let metadata = ZimFileService.getMetaData(url: url),
let fileURLBookmark = ZimFileService.getBookmarkData(url: url) else { return nil }
@ -45,18 +45,24 @@ struct LibraryOperations {
zimFile.fileURLBookmark = fileURLBookmark
zimFile.isMissing = false
if context.hasChanges { try? context.save() }
DispatchQueue.main.async {
onComplete?()
}
}
return metadata
}
/// Reopen zim files from url bookmark data.
static func reopen() {
static func reopen(onComplete: (() -> Void)?) {
var successCount = 0
let context = Database.shared.container.viewContext
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
guard let data = zimFile.fileURLBookmark else { return }
do {
@ -77,6 +83,7 @@ struct LibraryOperations {
}
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

View File

@ -13,6 +13,7 @@ import WebKit
import Defaults
import OrderedCollections
import CoreKiwix
// swiftlint:disable file_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 outlineItems = [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?
private(set) var tabID: NSManagedObjectID? {
@ -67,18 +78,26 @@ final class BrowserViewModel: NSObject, ObservableObject,
private var canGoBackObserver: NSKeyValueObservation?
private var canGoForwardObserver: NSKeyValueObservation?
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
static var urlForNewTab: URL?
private var cancellables: Set<AnyCancellable> = []
// MARK: - Lifecycle
init(tabID: NSManagedObjectID? = nil) {
self.tabID = tabID
webView = WKWebView(frame: .zero, configuration: WebViewConfiguration())
// Bookmark fetching:
bookmarkFetchedResultsController = NSFetchedResultsController(
fetchRequest: Bookmark.fetchRequest(), // initially empty
managedObjectContext: Database.viewContext,
sectionNameKeyPath: nil,
cacheName: nil
)
super.init()
bookmarkFetchedResultsController.delegate = self
// configure web view
webView.allowsBackForwardNavigationGestures = true
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 }
self?.didUpdate(title: title, url: url)
}
bookmarkFetchedResultsController?.delegate = self
}
private func didUpdate(title: String, url: URL) {
@ -138,17 +156,6 @@ final class BrowserViewModel: NSObject, ObservableObject,
tab.title = title
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() {
@ -170,6 +177,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
func load(url: URL) {
guard webView.url != url else { return }
webView.load(URLRequest(url: url))
self.url = url
}
func loadRandomArticle(zimFileID: UUID? = nil) {
@ -312,9 +320,10 @@ final class BrowserViewModel: NSObject, ObservableObject,
let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration())
webView.load(URLRequest(url: url))
return WebViewController(webView: webView)
}, actionProvider: { _ in
},
actionProvider: { _ in
var actions = [UIAction]()
// open url
actions.append(
UIAction(title: "Open".localized, image: UIImage(systemName: "doc.text")) { _ in
@ -326,19 +335,23 @@ final class BrowserViewModel: NSObject, ObservableObject,
NotificationCenter.openURL(url, inNewTab: true)
}
)
// bookmark
let bookmarkAction: UIAction = {
let context = Database.viewContext
let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg)
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,
image: UIImage(systemName: "star.slash.fill")) { [weak self] _ in
self?.deleteBookmark(url: url)
}
} 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)
}
}
@ -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
class NavigationViewModel: ObservableObject {
@Published var currentItem: NavigationItem?
@Published var readingURL: URL?
// remained optional due to focusedSceneValue conformance
@Published var currentItem: NavigationItem? = .loading
init() {
#if os(macOS)
currentItem = .reading
#elseif os(iOS)
navigateToMostRecentTab()
#endif
}
// MARK: - Tab Management
private func makeTab(context: NSManagedObjectContext) -> Tab {
@ -55,11 +47,9 @@ class NavigationViewModel: ObservableObject {
@MainActor
func tabIDFor(url: URL?) -> NSManagedObjectID {
guard let url else {
return createTab()
}
let coordinator = Database.viewContext.persistentStoreCoordinator
guard let tabID = coordinator?.managedObjectID(forURIRepresentation: url) else {
guard let url,
let coordinator = Database.viewContext.persistentStoreCoordinator,
let tabID = coordinator.managedObjectID(forURIRepresentation: url) else {
return createTab()
}
return tabID

View File

@ -42,7 +42,7 @@ struct BrowserTab: View {
.searchable(text: $search.searchText, placement: .toolbar)
.modify { view in
#if os(macOS)
view.navigationTitle(browser.articleTitle.isEmpty ? "Kiwix" : browser.articleTitle)
view.navigationTitle(browser.articleTitle.isEmpty ? Brand.appName : browser.articleTitle)
.navigationSubtitle(browser.zimFileName)
#elseif os(iOS)
view
@ -59,7 +59,7 @@ struct BrowserTab: View {
struct Content: View {
@Environment(\.isSearching) private var isSearching
@EnvironmentObject private var browser: BrowserViewModel
var body: some View {
GeometryReader { proxy in
Group {
@ -70,7 +70,7 @@ struct BrowserTab: View {
#elseif os(iOS)
.environment(\.horizontalSizeClass, proxy.size.width > 750 ? .regular : .compact)
#endif
} else if browser.url == nil {
} else if browser.url == nil && FeatureFlags.hasLibrary {
Welcome()
} else {
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 {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@EnvironmentObject private var browser: BrowserViewModel
@State private var isShowingBookmark = false
@State private var isShowingPopOver = false
var body: some View {
#if os(macOS)
@ -45,7 +45,7 @@ struct BookmarkButton: View {
}
}
Button {
isShowingBookmark = true
isShowingPopOver = true
} label: {
Label("Show Bookmarks".localized, systemImage: "list.star")
}
@ -57,15 +57,15 @@ struct BookmarkButton: View {
.renderingMode(browser.articleBookmarked ? .original : .template)
}
} primaryAction: {
isShowingBookmark = true
isShowingPopOver = true
}
.help("Show bookmarks. Long press to bookmark or unbookmark the current article.".localized)
.popover(isPresented: $isShowingBookmark) {
.popover(isPresented: $isShowingPopOver) {
NavigationView {
Bookmarks().navigationBarTitleDisplayMode(.inline).toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isShowingBookmark = false
isShowingPopOver = false
} label: {
Text("Done".localized).fontWeight(.semibold)
}

View File

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

View File

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

View File

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

View File

@ -25,19 +25,23 @@ struct ReadingSettings: View {
Button("Reset".localized) { webViewPageZoom = 1 }.disabled(webViewPageZoom == 1)
}
}
SettingSection(name: "External link".localized) {
Picker(selection: $externalLinkLoadingPolicy) {
ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in
Text(loadingPolicy.name.localized).tag(loadingPolicy)
}
} label: { }
if FeatureFlags.showExternalLinkOptionInSettings {
SettingSection(name: "External link".localized) {
Picker(selection: $externalLinkLoadingPolicy) {
ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in
Text(loadingPolicy.name.localized).tag(loadingPolicy)
}
} label: { }
}
}
SettingSection(name: "Search snippet".localized) {
Picker(selection: $searchResultSnippetMode) {
ForEach(SearchResultSnippetMode.allCases) { snippetMode in
Text(snippetMode.name.localized).tag(snippetMode)
}
} label: { }
if FeatureFlags.showSearchSnippetInSettings {
SettingSection(name: "Search snippet".localized) {
Picker(selection: $searchResultSnippetMode) {
ForEach(SearchResultSnippetMode.allCases) { snippetMode in
Text(snippetMode.name.localized).tag(snippetMode)
}
} label: { }
}
}
}
.padding()
@ -118,15 +122,24 @@ struct Settings: View {
}
var body: some View {
List {
readingSettings
librarySettings
catalogSettings
backupSettings
miscellaneous
if FeatureFlags.hasLibrary {
List {
readingSettings
librarySettings
catalogSettings
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 {
@ -135,14 +148,18 @@ struct Settings: View {
Text("Page zoom".localized +
": \(Formatter.percent.string(from: NSNumber(value: webViewPageZoom)) ?? "")")
}
Picker("External link".localized, selection: $externalLinkLoadingPolicy) {
ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in
Text(loadingPolicy.name.localized).tag(loadingPolicy)
if FeatureFlags.showExternalLinkOptionInSettings {
Picker("External link".localized, selection: $externalLinkLoadingPolicy) {
ForEach(ExternalLinkLoadingPolicy.allCases) { loadingPolicy in
Text(loadingPolicy.name.localized).tag(loadingPolicy)
}
}
}
Picker("Search snippet".localized, selection: $searchResultSnippetMode) {
ForEach(SearchResultSnippetMode.allCases) { snippetMode in
Text(snippetMode.name.localized).tag(snippetMode)
if FeatureFlags.showSearchSnippetInSettings {
Picker("Search snippet".localized, selection: $searchResultSnippetMode) {
ForEach(SearchResultSnippetMode.allCases) { snippetMode in
Text(snippetMode.name.localized).tag(snippetMode)
}
}
}
}
@ -203,7 +220,8 @@ struct Settings: View {
Section("Misc".localized) {
Button("Feedback".localized) { UIApplication.shared.open(URL(string: "mailto:feedback@kiwix.org")!) }
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)
}
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 {
VStack(spacing: 6) {
Image("Kiwix_logo_v3")
Image(Brand.welcomeLogoImageName)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(width: 60, height: 60)
.background(RoundedRectangle(cornerRadius: 10, style: .continuous).foregroundColor(.white))
Text("KIWIX").font(.largeTitle).fontWeight(.bold)
Text(Brand.appName.uppercased()).font(.largeTitle).fontWeight(.bold)
}
}