From 8132e31b893055ceb7f2a68fc71d487fc8cbc1c8 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Sat, 11 Nov 2023 19:20:29 +0000 Subject: [PATCH 01/28] Swicth to LibraryPtr See https://github.com/kiwix/libkiwix/commit/1dc97055974a95245e17d14cf27e6376cb8ca416 --- Model/OPDSParser/OPDSParser.mm | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Model/OPDSParser/OPDSParser.mm b/Model/OPDSParser/OPDSParser.mm index 5d309824..6dd3fd73 100644 --- a/Model/OPDSParser/OPDSParser.mm +++ b/Model/OPDSParser/OPDSParser.mm @@ -18,7 +18,7 @@ @interface OPDSParser () -@property kiwix::Library *library; +@property kiwix::LibraryPtr library; @end @@ -27,15 +27,11 @@ - (instancetype _Nonnull)init { self = [super init]; if (self) { - self.library = new kiwix::Library(); + self.library = kiwix::Library::create(); } return self; } -- (void)dealloc { - delete self.library; -} - - (BOOL)parseData:(nonnull NSData *)data { try { NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; From e0aa5876c1958b5885c5868ee7f6983e31d63c12 Mon Sep 17 00:00:00 2001 From: Emmanuel Engelhart Date: Sat, 4 Nov 2023 10:22:40 +0100 Subject: [PATCH 02/28] Multiple clarifications around system support --- README.md | 87 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 1a765465..49980289 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,74 @@ -# Kiwix for iOS & macOS +# Kiwix for Apple iOS & macOS -This is the home for Kiwix apps on iOS and macOS. +This is the home for Kiwix apps for Apple iOS and macOS. [![CodeFactor](https://www.codefactor.io/repository/github/kiwix/apple/badge)](https://www.codefactor.io/repository/github/kiwix/apple) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) Drawing= -### Mobile app for iPads & iPhones ### +## Download + +Kiwix apps are made available primarily via the [Mac App +Store](https://macos.kiwix.org). + +Most recent versions of Kiwix support the three latest versions of the +OSes (either iOS or macOS). Older versions of Kiwix being still +downloadable for older versions of macOS and iOS on the Mac App Store. + +### iPads & iPhones ### - Download the iOS mobile app on the [App Store](https://ios.kiwix.org) -### Kiwix Desktop for macOS ### +### macOS ### - Download Kiwix Desktop on the [Mac App Store](https://macos.kiwix.org) - Download Kiwix Desktop [DMG file](https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos.dmg) -## Developers +## Develop + +Kiwix developers use to work with cutting-edge versions of both macOS +and Xcode. [Continuous +integration](https://en.wikipedia.org/wiki/Continuous_integration) +secures that the whole project still compiles on the next to last +version of macOS with latest version of Xcode distributed on it. + +### CPU Architectures + +Kiwix compiles on both macOS with x86_64 or arm64 (M1, M2, ... family). + +Kiwix for iOS and macOS can run, in both cases, on x86_64 or arm64. ### Dependencies +To compile Kiwix you rely on the following compilation tools: * An [Apple Developer account](https://developer.apple.com) (doesn't require membership) * Latest Apple Developers Tools ([Xcode](https://developer.apple.com/xcode/)) * Its command-line utilities (`xcode-select --install`) -* `CoreKiwix.xcframework` ([libkiwix](https://github.com/kiwix/libkiwix)) +* `CoreKiwix.xcframework` ([libkiwix](https://github.com/kiwix/libkiwix) and [libzim](https://github.com/openzim/libzim)) -### Creating `CoreKiwix.xcframework` +### Steps -Instructions to build libkiwix at [on the kiwix-build repo](https://github.com/kiwix/kiwix-build). +To compile Kiwix, follow these steps: +* Open project with Xcode `open Kiwix.xcodeproj/project.xcworkspace/` +* Change the Bundle Identifier (in *Signing & Capabilities*) +* Select appropriate Signing Certificate/Profile. -The xcframework is a bundle of a library for multiple architectures and/or platforms. The `CoreKiwix.xcframework` will contain libkiwix library for macOS archs and for iOS. You don't have to follow steps for other platform/arch if you don't need them. +## Compile `CoreKiwix.xcframework` yourself -Following steps are done from kiwix-build root and assume your apple repository is at `../apple`. +`CoreKiwix.xcframework` is [made +available](https://dev.kiwix.org/apple/CoreKiwix.xcframework.zip) for +all supported OSes and CPU architectures. But you might want to +compile this piece (C++ code) by yourself. Here follow the +instructions to build libkiwix at [on the kiwix-build +repo](https://github.com/kiwix/kiwix-build). -#### Build libkiwix +The xcframework is a bundle of a library for multiple architectures +and/or platforms. The `CoreKiwix.xcframework` will contain libkiwix +library for macOS archs and for iOS. You don't have to follow steps +for other platform/arch if you don't need them. + +Following steps are done from kiwix-build root and assume your apple +repository is at `../apple`. + +### Build libkiwix Make sure to preinstall kiwix-build prerequisites (ninja and meson). @@ -40,7 +78,8 @@ If you use homebrew, run the following brew install ninja meson ``` -Make sure xcode command tools are installed. Make sure to download an iOS SDK if you want to build for iOS. +Make sure Xcode command tools are installed. Make sure to download an +iOS SDK if you want to build for iOS. ```sh xcode-select --install @@ -59,10 +98,11 @@ kiwix-build --target-platform macOS_x86_64 libkiwix kiwix-build --target-platform macOS_arm64_static libkiwix ``` -#### Create fat archive with all dependencies +### Create fat archive with all dependencies -This creates a single `.a` archive named `merged.a` (for each platform) which contains libkiwix and all it's dependencies. -Skip those you don't want to support. +This creates a single `.a` archive named `merged.a` (for each +platform) which contains libkiwix and all it's dependencies. Skip +those you don't want to support. ```sh libtool -static -o BUILD_macOS_x86_64/INSTALL/lib/merged.a BUILD_macOS_x86_64/INSTALL/lib/*.a @@ -71,7 +111,9 @@ libtool -static -o BUILD_iOS_x86_64/INSTALL/lib/merged.a BUILD_iOS_x86_64/INSTAL libtool -static -o BUILD_iOS_arm64/INSTALL/lib/merged.a BUILD_iOS_arm64/INSTALL/lib/*.a ``` -If you built macOS support for both archs (that's what you want unless you know what you're doing), you need to merge both files into a single one +If you built macOS support for both archs (that's what you want unless +you know what you're doing), you need to merge both files into a +single one ```sh mkdir -p macOS_fat @@ -80,7 +122,7 @@ lipo -create -output macOS_fat/merged.a \ -arch arm64 BUILD_macOS_arm64_static/INSTALL/lib/merged.a ``` -#### Add fat archive to xcframework +### Add fat archive to xcframework ```sh xcodebuild -create-xcframework \ @@ -90,11 +132,10 @@ xcodebuild -create-xcframework \ -output ../apple/CoreKiwix.xcframework ``` -You can now launch the build from Xcode and use the iOS simulator or your macOS target. At this point the xcframework is not signed. +You can now launch the build from Xcode and use the iOS simulator or +your macOS target. At this point the xcframework is not signed. +## License -### Building Kiwix iOS or Kiwix macOS - -* Open project with Xcode `open Kiwix.xcodeproj/project.xcworkspace/` -* Change the Bundle Identifier (in *Signing & Capabilities*) -* Select appropriate Signing Certificate/Profile. +[GPLv3](https://www.gnu.org/licenses/gpl-3.0) or later, see +[LICENSE](LICENSE) for more details. \ No newline at end of file From 67591eac0dcf6a1448fbd9028c80b0cadeb7e36f Mon Sep 17 00:00:00 2001 From: Emmanuel Engelhart Date: Sun, 5 Nov 2023 17:33:00 +0100 Subject: [PATCH 03/28] Add CoreKiwix/xcframework setup instruction --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 49980289..fada8e57 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ To compile Kiwix you rely on the following compilation tools: ### Steps To compile Kiwix, follow these steps: +* Put CoreKiwix/xcframework at the root of the root of code folder * Open project with Xcode `open Kiwix.xcodeproj/project.xcworkspace/` * Change the Bundle Identifier (in *Signing & Capabilities*) * Select appropriate Signing Certificate/Profile. From bd23f3f4d2d7407829a7bb747ba9bb41d656339b Mon Sep 17 00:00:00 2001 From: Kelson Date: Sun, 5 Nov 2023 17:34:48 +0100 Subject: [PATCH 04/28] Update README.md Co-authored-by: rgaudin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fada8e57..44f508aa 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ To compile Kiwix, follow these steps: available](https://dev.kiwix.org/apple/CoreKiwix.xcframework.zip) for all supported OSes and CPU architectures. But you might want to compile this piece (C++ code) by yourself. Here follow the -instructions to build libkiwix at [on the kiwix-build +instructions to build libkiwix+libzim at [on the kiwix-build repo](https://github.com/kiwix/kiwix-build). The xcframework is a bundle of a library for multiple architectures From 3362c7e26caae78ddb339e3c6b8b4266e555f380 Mon Sep 17 00:00:00 2001 From: Kelson Date: Sun, 5 Nov 2023 17:35:06 +0100 Subject: [PATCH 05/28] Update README.md Co-authored-by: rgaudin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44f508aa..6c7f496f 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ compile this piece (C++ code) by yourself. Here follow the instructions to build libkiwix+libzim at [on the kiwix-build repo](https://github.com/kiwix/kiwix-build). -The xcframework is a bundle of a library for multiple architectures +The xcframework is a bundle of all libkiwix dependencies for multiple architectures and/or platforms. The `CoreKiwix.xcframework` will contain libkiwix library for macOS archs and for iOS. You don't have to follow steps for other platform/arch if you don't need them. From 32f497c85cdb5eee4670be39b13f6e392376e477 Mon Sep 17 00:00:00 2001 From: Kelson Date: Sun, 5 Nov 2023 17:35:52 +0100 Subject: [PATCH 06/28] Update README.md Co-authored-by: rgaudin --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c7f496f..1b1686a3 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ version of macOS with latest version of Xcode distributed on it. ### CPU Architectures -Kiwix compiles on both macOS with x86_64 or arm64 (M1, M2, ... family). +Kiwix compiles on both macOS architectures x86_64 and arm64 (Apple silicon). Kiwix for iOS and macOS can run, in both cases, on x86_64 or arm64. From 7ed437081dcf05ad0b2cde2cd11722133e365a68 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Thu, 16 Nov 2023 08:58:36 +0000 Subject: [PATCH 07/28] updated README to match CoreKiwix from kiwix-build --- README.md | 98 ++++++++++++++----------------------------------------- 1 file changed, 25 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 1b1686a3..0cfdb29a 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,15 @@ This is the home for Kiwix apps for Apple iOS and macOS. ## Download -Kiwix apps are made available primarily via the [Mac App -Store](https://macos.kiwix.org). +Kiwix apps are made available primarily via the [App Store](https://ios.kiwix.org) and [Mac App Store](https://macos.kiwix.org). macOS version can also be [downloaded directly](https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos.dmg). -Most recent versions of Kiwix support the three latest versions of the +Most recent versions of Kiwix support the three latest major versions of the OSes (either iOS or macOS). Older versions of Kiwix being still downloadable for older versions of macOS and iOS on the Mac App Store. -### iPads & iPhones ### -- Download the iOS mobile app on the [App Store](https://ios.kiwix.org) - -### macOS ### -- Download Kiwix Desktop on the [Mac App Store](https://macos.kiwix.org) -- Download Kiwix Desktop [DMG file](https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos.dmg) - ## Develop -Kiwix developers use to work with cutting-edge versions of both macOS -and Xcode. [Continuous -integration](https://en.wikipedia.org/wiki/Continuous_integration) -secures that the whole project still compiles on the next to last -version of macOS with latest version of Xcode distributed on it. +Kiwix developers usually work with latest macOS and Xcode. Check our [Continuous Integration Workflow](https://github.com/kiwix/apple/blob/main/.github/workflows/ci.yml) to find out which XCode version we use on Github Actions. ### CPU Architectures @@ -39,6 +27,7 @@ Kiwix for iOS and macOS can run, in both cases, on x86_64 or arm64. ### Dependencies To compile Kiwix you rely on the following compilation tools: + * An [Apple Developer account](https://developer.apple.com) (doesn't require membership) * Latest Apple Developers Tools ([Xcode](https://developer.apple.com/xcode/)) * Its command-line utilities (`xcode-select --install`) @@ -47,33 +36,29 @@ To compile Kiwix you rely on the following compilation tools: ### Steps To compile Kiwix, follow these steps: -* Put CoreKiwix/xcframework at the root of the root of code folder + +* Put `CoreKiwix/xcframework` at the root of this folder * Open project with Xcode `open Kiwix.xcodeproj/project.xcworkspace/` * Change the Bundle Identifier (in *Signing & Capabilities*) * Select appropriate Signing Certificate/Profile. -## Compile `CoreKiwix.xcframework` yourself +### Getting `CoreKiwix.xcframework` -`CoreKiwix.xcframework` is [made -available](https://dev.kiwix.org/apple/CoreKiwix.xcframework.zip) for -all supported OSes and CPU architectures. But you might want to -compile this piece (C++ code) by yourself. Here follow the -instructions to build libkiwix+libzim at [on the kiwix-build -repo](https://github.com/kiwix/kiwix-build). +`CoreKiwix.xcframework` is published with all supported platforms and CPU architectures: + +- [latest release](https://download.kiwix.org/release/libkiwix/libkiwix_xcframework.tar.gz) +- [latest nightly](https://download.kiwix.org/nightly/libkiwix_xcframework.tar.gz): using `main` branch of both `libkiwix` and `libzim`. + +#### Compiling `CoreKiwix.xcframework` + +You may want to compile it yourself, to use different branches of said projects for instance. The xcframework is a bundle of all libkiwix dependencies for multiple architectures -and/or platforms. The `CoreKiwix.xcframework` will contain libkiwix -library for macOS archs and for iOS. You don't have to follow steps -for other platform/arch if you don't need them. +and platforms. The `CoreKiwix.xcframework` will contain libkiwix +library for macOS archs and for iOS. It is built off [kiwix-build +repo](https://github.com/kiwix/kiwix-build). -Following steps are done from kiwix-build root and assume your apple -repository is at `../apple`. - -### Build libkiwix - -Make sure to preinstall kiwix-build prerequisites (ninja and meson). - -If you use homebrew, run the following +Make sure to preinstall kiwix-build prerequisites (ninja and meson). If you use homebrew, run the following ```sh brew install ninja meson @@ -91,46 +76,13 @@ Then you can build `libkiwix` ```sh git clone https://github.com/kiwix/kiwix-build.git cd kiwix-build -# [iOS] build libkiwix -kiwix-build --target-platform iOS_arm64 libkiwix -kiwix-build --target-platform iOS_x86_64 libkiwix # iOS simulator in Xcode -# [macOS] build libkiwix -kiwix-build --target-platform macOS_x86_64 libkiwix -kiwix-build --target-platform macOS_arm64_static libkiwix -``` +python3 -m venv .venv +source .venv/bin/activate +pip install -e . -### Create fat archive with all dependencies - -This creates a single `.a` archive named `merged.a` (for each -platform) which contains libkiwix and all it's dependencies. Skip -those you don't want to support. - -```sh -libtool -static -o BUILD_macOS_x86_64/INSTALL/lib/merged.a BUILD_macOS_x86_64/INSTALL/lib/*.a -libtool -static -o BUILD_macOS_arm64_static/INSTALL/lib/merged.a BUILD_macOS_arm64_static/INSTALL/lib/*.a -libtool -static -o BUILD_iOS_x86_64/INSTALL/lib/merged.a BUILD_iOS_x86_64/INSTALL/lib/*.a -libtool -static -o BUILD_iOS_arm64/INSTALL/lib/merged.a BUILD_iOS_arm64/INSTALL/lib/*.a -``` - -If you built macOS support for both archs (that's what you want unless -you know what you're doing), you need to merge both files into a -single one - -```sh -mkdir -p macOS_fat -lipo -create -output macOS_fat/merged.a \ - -arch x86_64 BUILD_macOS_x86_64/INSTALL/lib/merged.a \ - -arch arm64 BUILD_macOS_arm64_static/INSTALL/lib/merged.a -``` - -### Add fat archive to xcframework - -```sh -xcodebuild -create-xcframework \ - -library macOS_fat/merged.a -headers BUILD_macOS_x86_64/INSTALL/include \ - -library BUILD_iOS_x86_64/INSTALL/lib/merged.a -headers BUILD_iOS_x86_64/INSTALL/include \ - -library BUILD_iOS_arm64/INSTALL/lib/merged.a -headers BUILD_iOS_arm64/INSTALL/include \ - -output ../apple/CoreKiwix.xcframework +kiwix-build --target-platform apple_all_static libkiwix +# assuming your kiwix-build and apple folder at at same level +cp -r BUILD_apple_all_static/INSTALL/lib/CoreKiwix.xcframework ../apple/ ``` You can now launch the build from Xcode and use the iOS simulator or From c64044d2431a9b630ce89779f619f517e14cafd5 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Mon, 13 Nov 2023 13:58:57 +0000 Subject: [PATCH 08/28] added dummy CI workflow --- .github/ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/ci.yml diff --git a/.github/ci.yml b/.github/ci.yml new file mode 100644 index 00000000..16360993 --- /dev/null +++ b/.github/ci.yml @@ -0,0 +1,15 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - ci + +jobs: + test: + runs-on: ubuntu-22-04 + steps: + - name: Hello + run: echo "hello world" From aa58f2b57bd106ea451dc57870f97be7256d0637 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Mon, 13 Nov 2023 14:00:33 +0000 Subject: [PATCH 09/28] dummy action in correct location --- .github/{ => workflows}/ci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/ci.yml (100%) diff --git a/.github/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/ci.yml rename to .github/workflows/ci.yml From 001136d59e62c43e202fd1a7fb5adddbc1796598 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Mon, 13 Nov 2023 14:03:19 +0000 Subject: [PATCH 10/28] fixed image name --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16360993..c12cba13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: test: - runs-on: ubuntu-22-04 + runs-on: ubuntu-22.04 steps: - name: Hello run: echo "hello world" From 9298b978a541553328018b86c3ab2ea6f1627adf Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Mon, 13 Nov 2023 13:52:25 +0000 Subject: [PATCH 11/28] Introducing Continuous Integration Building both macOS App and iOS App on every commit on `main` and on PRs. Build is not used/uploaded but still requires to be signed. Signing is done by automatically requesting appropriate Dev certificate using Store API Key. --- .github/workflows/ci.yml | 60 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c12cba13..d53f113b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,60 @@ on: - ci jobs: - test: - runs-on: ubuntu-22.04 + build: + strategy: + fail-fast: false + matrix: + destination: + - platform: macOS + name: Any Mac + - platform: iOS + name: Any iOS Device + runs-on: macos-13 + env: + XCF_URL: https://tmp.kiwix.org/ci/dev_preview/xcframework/libkiwix_xcframework-2023-11-11.tar.gz + # XCF_URL: https://download.kiwix.org/nightly/libkiwix_xcframework-2023-11-11.tar.gz + XC_PROJECT: Kiwix.xcodeproj + XC_SCHEME: Kiwix + XC_CONFIG: Release + XC_DESTINATION: platform=${{ matrix.destination.platform }},name=${{ matrix.destination.name }} + STORE_AUTH_KEY: /tmp/authkey.p8 steps: - - name: Hello - run: echo "hello world" + - name: Update Apple Intermediate Certificate + run: | + curl -L -o ~/Downloads/AppleWWDRCAG3.cer https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer + sudo security import ~/Downloads/AppleWWDRCAG3.cer \ + -k /Library/Keychains/System.keychain \ + -T /usr/bin/codesign \ + -T /usr/bin/security \ + -T /usr/bin/productbuild + - name: Set Xcode version (15.0.1) + # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode + run: sudo xcode-select -s /Applications/Xcode_15.0.1.app + - name: Checkout code + uses: actions/checkout@v3 + - name: Download CoreKiwix.xcframework + run: curl -L -o - $XCF_URL | tar -x --strip-components 2 + - name: Prepare Xcode + run: xcrun xcodebuild -checkFirstLaunchStatus || xcrun xcodebuild -runFirstLaunch + - name: Dump build settings + run: xcrun xcodebuild -project Kiwix.xcodeproj -scheme Kiwix -showBuildSettings + - name: Copy Key file to disk + run: echo "%{{ secrets.APPLE_STORE_AUTH_KEY }}" > ${STORE_AUTH_KEY} && chmod 600 ${STORE_AUTH_KEY} + - name: Build for ${{ matrix.destination.platform }}/${{ matrix.destination.name }} + env: + APPLE_STORE_AUTH_KEY_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ID }} + APPLE_STORE_AUTH_KEY_ISSUER_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ISSUER_ID }} + run: xcrun xcodebuild \ + -project ${XC_PROJECT} \ + -scheme ${XC_SCHEME} \ + -destination "${XC_DESTINATION}" \ + -configuration ${XC_CONFIG} \ + -onlyUsePackageVersionsFromResolvedFile \ + -derivedDataPath $PWD/build \ + -allowProvisioningUpdates \ + -authenticationKeyPath ${STORE_AUTH_KEY} \ + -authenticationKeyID ${APPLE_STORE_AUTH_KEY_ID} \ + -authenticationKeyIssuerID ${APPLE_STORE_AUTH_KEY_ISSUER_ID} \ + -verbose \ + build From 0a3c36417fd78560be7ff920e2d92d2b650467ab Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Mon, 13 Nov 2023 14:08:52 +0000 Subject: [PATCH 12/28] GH runner already has intermediate certificate --- .github/workflows/ci.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d53f113b..9633f3a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: XCF_URL: https://tmp.kiwix.org/ci/dev_preview/xcframework/libkiwix_xcframework-2023-11-11.tar.gz # XCF_URL: https://download.kiwix.org/nightly/libkiwix_xcframework-2023-11-11.tar.gz XC_PROJECT: Kiwix.xcodeproj + XC_WORKSPACE: Kiwix.xcodeproj/project.xcworkspace/ XC_SCHEME: Kiwix XC_CONFIG: Release XC_DESTINATION: platform=${{ matrix.destination.platform }},name=${{ matrix.destination.name }} @@ -34,7 +35,7 @@ jobs: -k /Library/Keychains/System.keychain \ -T /usr/bin/codesign \ -T /usr/bin/security \ - -T /usr/bin/productbuild + -T /usr/bin/productbuild || true - name: Set Xcode version (15.0.1) # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode run: sudo xcode-select -s /Applications/Xcode_15.0.1.app @@ -45,23 +46,11 @@ jobs: - name: Prepare Xcode run: xcrun xcodebuild -checkFirstLaunchStatus || xcrun xcodebuild -runFirstLaunch - name: Dump build settings - run: xcrun xcodebuild -project Kiwix.xcodeproj -scheme Kiwix -showBuildSettings + run: xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -showBuildSettings - name: Copy Key file to disk - run: echo "%{{ secrets.APPLE_STORE_AUTH_KEY }}" > ${STORE_AUTH_KEY} && chmod 600 ${STORE_AUTH_KEY} + run: echo "${{ secrets.APPLE_STORE_AUTH_KEY }}" > ${STORE_AUTH_KEY} && chmod 600 ${STORE_AUTH_KEY} - name: Build for ${{ matrix.destination.platform }}/${{ matrix.destination.name }} env: APPLE_STORE_AUTH_KEY_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ID }} APPLE_STORE_AUTH_KEY_ISSUER_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ISSUER_ID }} - run: xcrun xcodebuild \ - -project ${XC_PROJECT} \ - -scheme ${XC_SCHEME} \ - -destination "${XC_DESTINATION}" \ - -configuration ${XC_CONFIG} \ - -onlyUsePackageVersionsFromResolvedFile \ - -derivedDataPath $PWD/build \ - -allowProvisioningUpdates \ - -authenticationKeyPath ${STORE_AUTH_KEY} \ - -authenticationKeyID ${APPLE_STORE_AUTH_KEY_ID} \ - -authenticationKeyIssuerID ${APPLE_STORE_AUTH_KEY_ISSUER_ID} \ - -verbose \ - build + run: xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -destination "$XC_DESTINATION" -configuration $XC_CONFIG -onlyUsePackageVersionsFromResolvedFile -derivedDataPath $PWD/build -allowProvisioningUpdates -authenticationKeyPath $STORE_AUTH_KEY -authenticationKeyID $APPLE_STORE_AUTH_KEY_ID -authenticationKeyIssuerID $APPLE_STORE_AUTH_KEY_ISSUER_ID -verbose build From da67eefdc574b84918320510b03a3117c3218ba9 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Mon, 13 Nov 2023 16:34:38 +0000 Subject: [PATCH 13/28] import apple-development certificate --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9633f3a2..24b3e7f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,31 @@ jobs: XC_CONFIG: Release XC_DESTINATION: platform=${{ matrix.destination.platform }},name=${{ matrix.destination.name }} STORE_AUTH_KEY: /tmp/authkey.p8 + CERTIFICATE: /tmp/apple-development.p12 + SIGNING_IDENTITY: ${{ secrets.APPLE_DEVELOPMENT_SIGNING_IDENTITY }} + KEYCHAIN: /Users/runner/build.keychain-db + KEYCHAIN_PASSWORD: mysecretpassword + KEYCHAIN_PROFILE: build-profile steps: + - name: install Apple certificate + shell: bash + run: | + echo "${{ secrets.APPLE_DEVELOPMENT_SIGNING_CERTIFICATE }}" | base64 --decode -o $CERTIFICATE + security create-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN + security default-keychain -s $KEYCHAIN + security set-keychain-settings $KEYCHAIN + security unlock-keychain -p $KEYCHAIN_PASSWORD $KEYCHAIN + security import $CERTIFICATE -k $KEYCHAIN -P "${{ secrets.APPLE_DEVELOPMENT_SIGNING_P12_PASSWORD }}" -A -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild + rm $CERTIFICATE + security set-key-partition-list -S apple-tool:,apple: -s -k $KEYCHAIN_PASSWORD $KEYCHAIN + security find-identity -v $KEYCHAIN + xcrun notarytool store-credentials \ + --apple-id "${{ secrets.APPLE_SIGNING_ALTOOL_USERNAME }}" \ + --password "${{ secrets.APPLE_SIGNING_ALTOOL_PASSWORD }}" \ + --team-id "${{ secrets.APPLE_SIGNING_TEAM }}" \ + --validate \ + --keychain $KEYCHAIN \ + $KEYCHAIN_PROFILE - name: Update Apple Intermediate Certificate run: | curl -L -o ~/Downloads/AppleWWDRCAG3.cer https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer From f8c863d29a8687e97a79e3d50ef3c4762334170c Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Tue, 14 Nov 2023 11:29:35 +0000 Subject: [PATCH 14/28] mark CoreKiwix.xcframework relative to root --- Kiwix.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kiwix.xcodeproj/project.pbxproj b/Kiwix.xcodeproj/project.pbxproj index 922d260d..2e2bc8ae 100644 --- a/Kiwix.xcodeproj/project.pbxproj +++ b/Kiwix.xcodeproj/project.pbxproj @@ -223,7 +223,7 @@ 97DA90D72975B0C100738365 /* LibraryRefreshViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRefreshViewModelTest.swift; sourceTree = ""; }; 97DE2BA1283A8E5C00C63D9B /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; 97DE2BA4283A944100C63D9B /* GridCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridCommon.swift; sourceTree = ""; }; - 97E88F4C2AE407320037F0E5 /* CoreKiwix.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = CoreKiwix.xcframework; sourceTree = ""; }; + 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 = ""; }; 97E94B22271EF250005B0295 /* Kiwix.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Kiwix.entitlements; sourceTree = ""; }; 97F3332E28AFC1A2007FF53C /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = ""; }; From 67a31f0a91fa35d65329c8e156066b40313b3c2a Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Wed, 15 Nov 2023 15:33:18 +0000 Subject: [PATCH 15/28] Running build up to twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only “reliable” way I found to succeed build in CI --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24b3e7f2..79e5ca64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,8 +73,11 @@ jobs: run: xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -showBuildSettings - name: Copy Key file to disk run: echo "${{ secrets.APPLE_STORE_AUTH_KEY }}" > ${STORE_AUTH_KEY} && chmod 600 ${STORE_AUTH_KEY} + - name: Install retry command + run: brew install kadwanev/brew/retry - name: Build for ${{ matrix.destination.platform }}/${{ matrix.destination.name }} env: APPLE_STORE_AUTH_KEY_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ID }} APPLE_STORE_AUTH_KEY_ISSUER_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ISSUER_ID }} - run: xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -destination "$XC_DESTINATION" -configuration $XC_CONFIG -onlyUsePackageVersionsFromResolvedFile -derivedDataPath $PWD/build -allowProvisioningUpdates -authenticationKeyPath $STORE_AUTH_KEY -authenticationKeyID $APPLE_STORE_AUTH_KEY_ID -authenticationKeyIssuerID $APPLE_STORE_AUTH_KEY_ISSUER_ID -verbose build + FRAMEWORK_SEARCH_PATHS: /Users/runner/work/apple/apple/ + run: retry -t 2 -- xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -destination "$XC_DESTINATION" -configuration $XC_CONFIG -onlyUsePackageVersionsFromResolvedFile -derivedDataPath $PWD/build -allowProvisioningUpdates -authenticationKeyPath $STORE_AUTH_KEY -authenticationKeyID $APPLE_STORE_AUTH_KEY_ID -authenticationKeyIssuerID $APPLE_STORE_AUTH_KEY_ISSUER_ID -verbose build From b3a3dbadc192cf526c6246fcb51b636bb1171b41 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Wed, 15 Nov 2023 16:07:38 +0000 Subject: [PATCH 16/28] Without auth key --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79e5ca64..964ab131 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: XC_SCHEME: Kiwix XC_CONFIG: Release XC_DESTINATION: platform=${{ matrix.destination.platform }},name=${{ matrix.destination.name }} - STORE_AUTH_KEY: /tmp/authkey.p8 CERTIFICATE: /tmp/apple-development.p12 SIGNING_IDENTITY: ${{ secrets.APPLE_DEVELOPMENT_SIGNING_IDENTITY }} KEYCHAIN: /Users/runner/build.keychain-db @@ -71,13 +70,9 @@ jobs: run: xcrun xcodebuild -checkFirstLaunchStatus || xcrun xcodebuild -runFirstLaunch - name: Dump build settings run: xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -showBuildSettings - - name: Copy Key file to disk - run: echo "${{ secrets.APPLE_STORE_AUTH_KEY }}" > ${STORE_AUTH_KEY} && chmod 600 ${STORE_AUTH_KEY} - name: Install retry command run: brew install kadwanev/brew/retry - name: Build for ${{ matrix.destination.platform }}/${{ matrix.destination.name }} env: - APPLE_STORE_AUTH_KEY_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ID }} - APPLE_STORE_AUTH_KEY_ISSUER_ID: ${{ secrets.APPLE_STORE_AUTH_KEY_ISSUER_ID }} FRAMEWORK_SEARCH_PATHS: /Users/runner/work/apple/apple/ - run: retry -t 2 -- xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -destination "$XC_DESTINATION" -configuration $XC_CONFIG -onlyUsePackageVersionsFromResolvedFile -derivedDataPath $PWD/build -allowProvisioningUpdates -authenticationKeyPath $STORE_AUTH_KEY -authenticationKeyID $APPLE_STORE_AUTH_KEY_ID -authenticationKeyIssuerID $APPLE_STORE_AUTH_KEY_ISSUER_ID -verbose build + run: retry -t 2 -- xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -destination "$XC_DESTINATION" -configuration $XC_CONFIG -onlyUsePackageVersionsFromResolvedFile -derivedDataPath $PWD/build -allowProvisioningUpdates -verbose build From 55db5807df63ceb734eaa9ec09df66b586bde4d3 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Wed, 15 Nov 2023 16:19:57 +0000 Subject: [PATCH 17/28] additional comments --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 964ab131..0b83cf00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,7 @@ jobs: --validate \ --keychain $KEYCHAIN \ $KEYCHAIN_PROFILE + # not necessary on github runner but serves as documentation for local setup - name: Update Apple Intermediate Certificate run: | curl -L -o ~/Downloads/AppleWWDRCAG3.cer https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer @@ -70,6 +71,7 @@ jobs: run: xcrun xcodebuild -checkFirstLaunchStatus || xcrun xcodebuild -runFirstLaunch - name: Dump build settings run: xcrun xcodebuild -workspace $XC_WORKSPACE -scheme $XC_SCHEME -showBuildSettings + # build is launched up to twice as it's common the build fails, looking for CoreKiwix module - name: Install retry command run: brew install kadwanev/brew/retry - name: Build for ${{ matrix.destination.platform }}/${{ matrix.destination.name }} From b195baa5b9e3d77697b3a0c7b135c389a6effb73 Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Thu, 16 Nov 2023 09:53:35 +0000 Subject: [PATCH 18/28] using latest nightly redirect --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b83cf00..a70a2926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: push: branches: - main - - ci jobs: build: @@ -19,8 +18,7 @@ jobs: name: Any iOS Device runs-on: macos-13 env: - XCF_URL: https://tmp.kiwix.org/ci/dev_preview/xcframework/libkiwix_xcframework-2023-11-11.tar.gz - # XCF_URL: https://download.kiwix.org/nightly/libkiwix_xcframework-2023-11-11.tar.gz + XCF_URL: https://download.kiwix.org/nightly/libkiwix_xcframework.tar.gz XC_PROJECT: Kiwix.xcodeproj XC_WORKSPACE: Kiwix.xcodeproj/project.xcworkspace/ XC_SCHEME: Kiwix From 84fc4ac780a438f0c52979a17aff913b21d2827d Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Sat, 11 Nov 2023 19:20:29 +0000 Subject: [PATCH 19/28] Swicth to LibraryPtr See https://github.com/kiwix/libkiwix/commit/1dc97055974a95245e17d14cf27e6376cb8ca416 --- Model/OPDSParser/OPDSParser.mm | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Model/OPDSParser/OPDSParser.mm b/Model/OPDSParser/OPDSParser.mm index 5d309824..6dd3fd73 100644 --- a/Model/OPDSParser/OPDSParser.mm +++ b/Model/OPDSParser/OPDSParser.mm @@ -18,7 +18,7 @@ @interface OPDSParser () -@property kiwix::Library *library; +@property kiwix::LibraryPtr library; @end @@ -27,15 +27,11 @@ - (instancetype _Nonnull)init { self = [super init]; if (self) { - self.library = new kiwix::Library(); + self.library = kiwix::Library::create(); } return self; } -- (void)dealloc { - delete self.library; -} - - (BOOL)parseData:(nonnull NSData *)data { try { NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; From 66ee6f775928af5bae8cba6c585f2f18a5f08e3c Mon Sep 17 00:00:00 2001 From: renaud gaudin Date: Fri, 17 Nov 2023 16:10:20 +0000 Subject: [PATCH 20/28] using libkiwix release, set to 13.0.0 --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36f3ca91..ae6b1fa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,10 @@ on: push: branches: - main + +env: + LIBKIWIX_VERSION: "13.0.0" + jobs: build: strategy: @@ -17,8 +21,6 @@ jobs: name: Any iOS Device runs-on: macos-13 env: - XCF_URL: https://download.kiwix.org/nightly/libkiwix_xcframework.tar.gz - XC_PROJECT: Kiwix.xcodeproj XC_WORKSPACE: Kiwix.xcodeproj/project.xcworkspace/ XC_SCHEME: Kiwix XC_CONFIG: Release @@ -63,6 +65,8 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Download CoreKiwix.xcframework + env: + XCF_URL: https://download.kiwix.org/release/libkiwix/libkiwix_xcframework-${{ env.LIBKIWIX_VERSION }}.tar.gz run: curl -L -o - $XCF_URL | tar -x --strip-components 2 - name: Prepare Xcode run: xcrun xcodebuild -checkFirstLaunchStatus || xcrun xcodebuild -runFirstLaunch From fa16cfa28fe8245ab4da468e3a4e8f5b4ba774c0 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 13 Nov 2023 21:34:05 +0100 Subject: [PATCH 21/28] Fix opening new tabs on macos(#518) --- App/CompactViewController.swift | 2 +- Model/Utilities/URL.swift | 6 ++- ViewModel/BrowserViewModel.swift | 52 +++++++++++++++---- Views/BrowserTab.swift | 2 +- Views/ViewModifiers/ExternalLinkHandler.swift | 7 ++- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index 9cb0a84e..a1348726 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -130,7 +130,7 @@ private struct Content: View { .focusedSceneValue(\.browserViewModel, browser) .focusedSceneValue(\.canGoBack, browser.canGoBack) .focusedSceneValue(\.canGoForward, browser.canGoForward) - .modifier(ExternalLinkHandler()) + .modifier(ExternalLinkHandler(externalURL: browser.externalURL)) .onAppear { browser.updateLastOpened() } diff --git a/Model/Utilities/URL.swift b/Model/Utilities/URL.swift index a8a468f1..fed817ed 100644 --- a/Model/Utilities/URL.swift +++ b/Model/Utilities/URL.swift @@ -16,6 +16,10 @@ extension URL { var isKiwixURL: Bool { return scheme?.caseInsensitiveCompare("kiwix") == .orderedSame } - + + var isExternal: Bool { + ["http", "https"].contains(scheme) + } + static let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) } diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 2354f6b4..87efe7e6 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -13,9 +13,9 @@ import WebKit import OrderedCollections -class BrowserViewModel: NSObject, ObservableObject, - WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, - NSFetchedResultsControllerDelegate +final class BrowserViewModel: NSObject, ObservableObject, + WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, + NSFetchedResultsControllerDelegate { static private var cache = OrderedDictionary() @@ -45,14 +45,17 @@ class BrowserViewModel: NSObject, ObservableObject, @Published private(set) var outlineItems = [OutlineItem]() @Published private(set) var outlineItemTree = [OutlineItem]() @Published private(set) var url: URL? - + @Published var externalURL: URL? + let tabID: NSManagedObjectID? let webView: WKWebView private var canGoBackObserver: NSKeyValueObservation? private var canGoForwardObserver: NSKeyValueObservation? private var titleURLObserver: AnyCancellable? private var bookmarkFetchedResultsController: NSFetchedResultsController? - + /// A temporary placeholder for the url that should be opened in a new tab, set on macOS only + private static var urlForNewTab: URL? + // MARK: - Lifecycle init(tabID: NSManagedObjectID? = nil) { @@ -66,7 +69,11 @@ class BrowserViewModel: NSObject, ObservableObject, webView.interactionState = tab.interactionState url = webView.url } - + if let urlForNewTab = Self.urlForNewTab { + url = urlForNewTab + load(url: urlForNewTab) + } + // configure web view webView.allowsBackForwardNavigationGestures = true webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile // for font adjustment to work @@ -178,8 +185,8 @@ class BrowserViewModel: NSObject, ObservableObject, decisionHandler(.cancel) } else if url.isKiwixURL { decisionHandler(.allow) - } else if url.scheme == "http" || url.scheme == "https" { - NotificationCenter.default.post(name: .externalLink, object: nil, userInfo: ["url": url]) + } else if url.isExternal { + externalURL = url decisionHandler(.cancel) } else if url.scheme == "geo" { if FeatureFlags.map { @@ -234,7 +241,34 @@ class BrowserViewModel: NSObject, ObservableObject, } // MARK: - WKUIDelegate - +#if os(macOS) + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + + guard navigationAction.targetFrame == nil else { return nil } + guard let newUrl = navigationAction.request.url else { return nil } + + // open external link in default browser + guard newUrl.isExternal == false else { + externalURL = newUrl + return nil + } + + // create new tab + guard let currentWindow = NSApp.keyWindow, + let windowController = currentWindow.windowController else { return nil } + // store the new url in a static way + Self.urlForNewTab = newUrl + // this creates a new BrowserViewModel + windowController.newWindowForTab(self) + // now reset the static url to nil, as the new BrowserViewModel already has it + Self.urlForNewTab = nil + guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { return nil } + currentWindow.addTabbedWindow(newWindow, ordered: .above) + return nil + } +#endif + #if os(iOS) func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, diff --git a/Views/BrowserTab.swift b/Views/BrowserTab.swift index 9a080449..2ade7102 100644 --- a/Views/BrowserTab.swift +++ b/Views/BrowserTab.swift @@ -38,7 +38,7 @@ struct BrowserTab: View { .focusedSceneValue(\.browserViewModel, browser) .focusedSceneValue(\.canGoBack, browser.canGoBack) .focusedSceneValue(\.canGoForward, browser.canGoForward) - .modifier(ExternalLinkHandler()) + .modifier(ExternalLinkHandler(externalURL: $browser.externalURL)) .searchable(text: $search.searchText, placement: .toolbar) .modify { view in #if os(macOS) diff --git a/Views/ViewModifiers/ExternalLinkHandler.swift b/Views/ViewModifiers/ExternalLinkHandler.swift index a3a1d0cb..e35e83fc 100644 --- a/Views/ViewModifiers/ExternalLinkHandler.swift +++ b/Views/ViewModifiers/ExternalLinkHandler.swift @@ -14,8 +14,7 @@ struct ExternalLinkHandler: ViewModifier { @State private var isAlertPresented = false @State private var activeAlert: ActiveAlert? @State private var activeSheet: ActiveSheet? - - private let externalLink = NotificationCenter.default.publisher(for: .externalLink) + @Binding var externalURL: URL? enum ActiveAlert { case ask(url: URL) @@ -28,8 +27,8 @@ struct ExternalLinkHandler: ViewModifier { } func body(content: Content) -> some View { - content.onReceive(externalLink) { notification in - guard let url = notification.userInfo?["url"] as? URL else { return } + content.onChange(of: externalURL) { url in + guard let url else { return } switch Defaults[.externalLinkLoadingPolicy] { case .alwaysAsk: isAlertPresented = true From 8f3cd6f371465cc632c0679a190824b2aad53e25 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 00:52:28 +0100 Subject: [PATCH 22/28] Create dedicated script handler and web delegates --- Kiwix.xcodeproj/project.pbxproj | 12 ++ ViewModel/BrowserNavDelegate.swift | 67 ++++++++ ViewModel/BrowserScriptHandler.swift | 85 ++++++++++ ViewModel/BrowserUIDelegate.swift | 90 ++++++++++ ViewModel/BrowserViewModel.swift | 238 +++------------------------ 5 files changed, 278 insertions(+), 214 deletions(-) create mode 100644 ViewModel/BrowserNavDelegate.swift create mode 100644 ViewModel/BrowserScriptHandler.swift create mode 100644 ViewModel/BrowserUIDelegate.swift diff --git a/Kiwix.xcodeproj/project.pbxproj b/Kiwix.xcodeproj/project.pbxproj index 2e2bc8ae..004dc2e5 100644 --- a/Kiwix.xcodeproj/project.pbxproj +++ b/Kiwix.xcodeproj/project.pbxproj @@ -98,6 +98,9 @@ 97E88F4D2AE407350037F0E5 /* CoreKiwix.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97E88F4C2AE407320037F0E5 /* CoreKiwix.xcframework */; }; 97F3333028AFC1A2007FF53C /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F3332E28AFC1A2007FF53C /* SearchResults.swift */; }; 97FB4ECE28B4E221003FB524 /* SwiftUIBackports in Frameworks */ = {isa = PBXBuildFile; productRef = 97FB4ECD28B4E221003FB524 /* SwiftUIBackports */; }; + 983ED6DC2B02E89300409078 /* BrowserScriptHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983ED6DB2B02E89300409078 /* BrowserScriptHandler.swift */; }; + 983ED6DE2B02ED4000409078 /* BrowserNavDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983ED6DD2B02ED4000409078 /* BrowserNavDelegate.swift */; }; + 983ED6E02B02EF1E00409078 /* BrowserUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983ED6DF2B02EF1E00409078 /* BrowserUIDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -231,6 +234,9 @@ 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 = ""; }; 97FD2F5E251EA07B0034927C /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; + 983ED6DB2B02E89300409078 /* BrowserScriptHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserScriptHandler.swift; sourceTree = ""; }; + 983ED6DD2B02ED4000409078 /* BrowserNavDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavDelegate.swift; sourceTree = ""; }; + 983ED6DF2B02EF1E00409078 /* BrowserUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserUIDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -317,6 +323,9 @@ 97176AD12A4FBD710093E3B0 /* BrowserViewModel.swift */, 97C13787284572AC00386C04 /* SearchViewModel.swift */, 97DE2BA1283A8E5C00C63D9B /* LibraryViewModel.swift */, + 983ED6DB2B02E89300409078 /* BrowserScriptHandler.swift */, + 983ED6DD2B02ED4000409078 /* BrowserNavDelegate.swift */, + 983ED6DF2B02EF1E00409078 /* BrowserUIDelegate.swift */, ); path = ViewModel; sourceTree = ""; @@ -789,6 +798,7 @@ 97F3333028AFC1A2007FF53C /* SearchResults.swift in Sources */, 972727AE2A897FAA00BCAF75 /* GridSection.swift in Sources */, 9709C0982A8E4C5700E4564C /* Commands.swift in Sources */, + 983ED6E02B02EF1E00409078 /* BrowserUIDelegate.swift in Sources */, 972727BF2A8A52DC00BCAF75 /* AlertHandler.swift in Sources */, 972096E72AE421C300B378B0 /* Attribute.swift in Sources */, 97341C6E2852248500BC273E /* DownloadTaskCell.swift in Sources */, @@ -806,6 +816,7 @@ 976D90DB281584BF00CC7D29 /* FlavorTag.swift in Sources */, 972DE4BD2814A5BE004FD9B9 /* OPDSParser.mm in Sources */, 972DE4B52814A502004FD9B9 /* Entities.swift in Sources */, + 983ED6DC2B02E89300409078 /* BrowserScriptHandler.swift in Sources */, 9721BBBB28427A93005C910D /* Bookmarks.swift in Sources */, 974E7EE92930201500BDF59C /* ZimFileService.swift in Sources */, 973A0DFD283100C300B41E71 /* ZimFilesOpened.swift in Sources */, @@ -820,6 +831,7 @@ 972727B12A898B9700BCAF75 /* NavigationButtons.swift in Sources */, 976F5EC62A97909100938490 /* BrowserTab.swift in Sources */, 9724FC3028D5F5BE001B7DD2 /* BookmarkContextMenu.swift in Sources */, + 983ED6DE2B02ED4000409078 /* BrowserNavDelegate.swift in Sources */, 976BAEBE284905760049404F /* SearchViewModel.swift in Sources */, 973A0DE7281DC8F400B41E71 /* DownloadService.swift in Sources */, 9721BBB72841C16D005C910D /* Message.swift in Sources */, diff --git a/ViewModel/BrowserNavDelegate.swift b/ViewModel/BrowserNavDelegate.swift new file mode 100644 index 00000000..70c323a2 --- /dev/null +++ b/ViewModel/BrowserNavDelegate.swift @@ -0,0 +1,67 @@ +// +// BrowserNavHandler.swift +// Kiwix +// +// Copyright © 2023 Chris Li. All rights reserved. +// + +import WebKit +import CoreLocation + +final class BrowserNavDelegate: NSObject, WKNavigationDelegate { + + @Published private(set) var externalURL: URL? + + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { decisionHandler(.cancel); return } + if url.isKiwixURL, let redirectedURL = ZimFileService.shared.getRedirectedURL(url: url) { + DispatchQueue.main.async { webView.load(URLRequest(url: redirectedURL)) } + decisionHandler(.cancel) + } else if url.isKiwixURL { + decisionHandler(.allow) + } else if url.isExternal { + externalURL = url + decisionHandler(.cancel) + } else if url.scheme == "geo" { + if FeatureFlags.map { + let _: CLLocation? = { + let parts = url.absoluteString.replacingOccurrences(of: "geo:", with: "").split(separator: ",") + guard let latitudeString = parts.first, + let longitudeString = parts.last, + let latitude = Double(latitudeString), + let longitude = Double(longitudeString) else { return nil } + return CLLocation(latitude: latitude, longitude: longitude) + }() + } else { + let coordinate = url.absoluteString.replacingOccurrences(of: "geo:", with: "") + if let url = URL(string: "http://maps.apple.com/?ll=\(coordinate)") { + #if os(macOS) + NSWorkspace.shared.open(url) + #elseif os(iOS) + UIApplication.shared.open(url) + #endif + } + } + decisionHandler(.cancel) + } else { + decisionHandler(.cancel) + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webView.evaluateJavaScript("expandAllDetailTags(); getOutlineItems();") + #if os(iOS) + webView.adjustTextSize() + #endif + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + let error = error as NSError + guard error.code != NSURLErrorCancelled else { return } + NotificationCenter.default.post( + name: .alert, object: nil, userInfo: ["rawValue": ActiveAlert.articleFailedToLoad.rawValue] + ) + } +} diff --git a/ViewModel/BrowserScriptHandler.swift b/ViewModel/BrowserScriptHandler.swift new file mode 100644 index 00000000..696abaf4 --- /dev/null +++ b/ViewModel/BrowserScriptHandler.swift @@ -0,0 +1,85 @@ +// +// BrowserScriptHandler.swift +// Kiwix +// +// Copyright © 2023 Chris Li. All rights reserved. +// + +import WebKit + +final class BrowserScriptHandler: NSObject, WKScriptMessageHandler { + @Published private(set) var outlineItems = [OutlineItem]() + @Published private(set) var outlineItemTree = [OutlineItem]() + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == "headings", let headings = message.body as? [[String: String]] { + DispatchQueue.global(qos: .userInitiated).async { + self.generateOutlineList(headings: headings) + self.generateOutlineTree(headings: headings) + } + } + } + + /// Convert flattened heading element data to a list of OutlineItems. + /// - Parameter headings: list of heading element data retrieved from webview + private func generateOutlineList(headings: [[String: String]]) { + let allLevels = headings.compactMap { Int($0["tag"]?.suffix(1) ?? "") } + let offset = allLevels.filter({ $0 == 1 }).count == 1 ? 2 : allLevels.min() ?? 0 + let outlineItems: [OutlineItem] = headings.enumerated().compactMap { index, heading in + guard let id = heading["id"], + let text = heading["text"], + let tag = heading["tag"], + let level = Int(tag.suffix(1)) else { return nil } + return OutlineItem(id: id, index: index, text: text, level: max(level - offset, 0)) + } + DispatchQueue.main.async { + self.outlineItems = outlineItems + } + } + + /// Convert flattened heading element data to a tree of OutlineItems. + /// - Parameter headings: list of heading element data retrieved from webview + private func generateOutlineTree(headings: [[String: String]]) { + let root = OutlineItem(index: -1, text: "", level: 0) + var stack: [OutlineItem] = [root] + var all = [String: OutlineItem]() + + headings.enumerated().forEach { index, heading in + guard let id = heading["id"], + let text = heading["text"], + let tag = heading["tag"], let level = Int(tag.suffix(1)) else { return } + let item = OutlineItem(id: id, index: index, text: text, level: level) + all[item.id] = item + + // get last item in stack + // if last item is child of item's sibling, unwind stack until a sibling is found + guard var lastItem = stack.last else { return } + while lastItem.level > item.level { + stack.removeLast() + lastItem = stack[stack.count - 1] + } + + // if item is last item's sibling, add item to parent and replace last item with itself in stack + // if item is last item's child, add item to parent and add item to stack + if lastItem.level == item.level { + stack[stack.count - 2].addChild(item) + stack[stack.count - 1] = item + } else if lastItem.level < item.level { + stack[stack.count - 1].addChild(item) + stack.append(item) + } + } + + // if there is only one h1, flatten one level + if let rootChildren = root.children, rootChildren.count == 1, let rootFirstChild = rootChildren.first { + let children = rootFirstChild.removeAllChildren() + DispatchQueue.main.async { + self.outlineItemTree = [rootFirstChild] + children + } + } else { + DispatchQueue.main.async { + self.outlineItemTree = root.children ?? [] + } + } + } +} diff --git a/ViewModel/BrowserUIDelegate.swift b/ViewModel/BrowserUIDelegate.swift new file mode 100644 index 00000000..0953382a --- /dev/null +++ b/ViewModel/BrowserUIDelegate.swift @@ -0,0 +1,90 @@ +// +// BrowserUIDelegate.swift +// Kiwix +// +// Copyright © 2023 Chris Li. All rights reserved. +// + +import WebKit + +final class BrowserUIDelegate: NSObject, WKUIDelegate { + + @Published private(set) var externalURL: URL? + +#if os(macOS) + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + + guard navigationAction.targetFrame == nil else { return nil } + guard let newUrl = navigationAction.request.url else { return nil } + + // open external link in default browser + guard newUrl.isExternal == false else { + externalURL = newUrl + return nil + } + + // create new tab + guard let currentWindow = NSApp.keyWindow, + let windowController = currentWindow.windowController else { return nil } + // store the new url in a static way + BrowserViewModel.urlForNewTab = newUrl + // this creates a new BrowserViewModel + windowController.newWindowForTab(self) + // now reset the static url to nil, as the new BrowserViewModel already has it + BrowserViewModel.urlForNewTab = nil + guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { return nil } + currentWindow.addTabbedWindow(newWindow, ordered: .above) + return nil + } +#endif + +#if os(iOS) + func webView(_ webView: WKWebView, + contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, + completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { + guard let url = elementInfo.linkURL, url.isKiwixURL else { completionHandler(nil); return } + let configuration = UIContextMenuConfiguration( + previewProvider: { + let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) + webView.load(URLRequest(url: url)) + return WebViewController(webView: webView) + }, actionProvider: { suggestedActions in + var actions = [UIAction]() + + // open url + actions.append( + UIAction(title: "Open", image: UIImage(systemName: "doc.text")) { _ in + webView.load(URLRequest(url: url)) + } + ) + actions.append( + UIAction(title: "Open in New Tab", image: UIImage(systemName: "doc.badge.plus")) { _ in + 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 { + return UIAction(title: "Remove Bookmark", image: UIImage(systemName: "star.slash.fill")) { _ in + self.deleteBookmark(url: url) + } + } else { + return UIAction(title: "Bookmark", image: UIImage(systemName: "star")) { _ in + self.createBookmark(url: url) + } + } + }() + actions.append(bookmarkAction) + + return UIMenu(children: actions) + } + ) + completionHandler(configuration) + } +#endif +} diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 87efe7e6..5aeb0b8a 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -14,7 +14,6 @@ import WebKit import OrderedCollections final class BrowserViewModel: NSObject, ObservableObject, - WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, NSFetchedResultsControllerDelegate { static private var cache = OrderedDictionary() @@ -53,16 +52,34 @@ final class BrowserViewModel: NSObject, ObservableObject, private var canGoForwardObserver: NSKeyValueObservation? private var titleURLObserver: AnyCancellable? private var bookmarkFetchedResultsController: NSFetchedResultsController? + private let scriptHandler: BrowserScriptHandler + private let navDelegate: BrowserNavDelegate + private let uiDelegate: BrowserUIDelegate /// A temporary placeholder for the url that should be opened in a new tab, set on macOS only - private static var urlForNewTab: URL? + static var urlForNewTab: URL? + private var cancellables: Set = [] // MARK: - Lifecycle init(tabID: NSManagedObjectID? = nil) { self.tabID = tabID self.webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) + scriptHandler = BrowserScriptHandler() + navDelegate = BrowserNavDelegate() + uiDelegate = BrowserUIDelegate() super.init() - + + scriptHandler.$outlineItems.assign(to: \.outlineItems, on: self) + .store(in: &cancellables) + scriptHandler.$outlineItemTree.assign(to: \.outlineItemTree, on: self) + .store(in: &cancellables) + + navDelegate.$externalURL.assign(to: \.externalURL, on: self) + .store(in: &cancellables) + + uiDelegate.$externalURL.assign(to: \.externalURL, on: self) + .store(in: &cancellables) + // restore webview state, and set url before observer call back // note: optionality of url determines what to show in a tab, so it should be set before tab is on screen if let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab { @@ -78,10 +95,10 @@ final class BrowserViewModel: NSObject, ObservableObject, webView.allowsBackForwardNavigationGestures = true webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile // for font adjustment to work webView.configuration.userContentController.removeScriptMessageHandler(forName: "headings") - webView.configuration.userContentController.add(self, name: "headings") - webView.navigationDelegate = self - webView.uiDelegate = self - + webView.configuration.userContentController.add(scriptHandler, name: "headings") + webView.navigationDelegate = navDelegate + webView.uiDelegate = uiDelegate + // get outline items if something is already loaded if webView.url != nil { webView.evaluateJavaScript("getOutlineItems();") @@ -174,150 +191,6 @@ final class BrowserViewModel: NSObject, ObservableObject, load(url: url) } - // MARK: - WKNavigationDelegate - - func webView(_ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - guard let url = navigationAction.request.url else { decisionHandler(.cancel); return } - if url.isKiwixURL, let redirectedURL = ZimFileService.shared.getRedirectedURL(url: url) { - DispatchQueue.main.async { webView.load(URLRequest(url: redirectedURL)) } - decisionHandler(.cancel) - } else if url.isKiwixURL { - decisionHandler(.allow) - } else if url.isExternal { - externalURL = url - decisionHandler(.cancel) - } else if url.scheme == "geo" { - if FeatureFlags.map { - let _: CLLocation? = { - let parts = url.absoluteString.replacingOccurrences(of: "geo:", with: "").split(separator: ",") - guard let latitudeString = parts.first, - let longitudeString = parts.last, - let latitude = Double(latitudeString), - let longitude = Double(longitudeString) else { return nil } - return CLLocation(latitude: latitude, longitude: longitude) - }() - } else { - let coordinate = url.absoluteString.replacingOccurrences(of: "geo:", with: "") - if let url = URL(string: "http://maps.apple.com/?ll=\(coordinate)") { - #if os(macOS) - NSWorkspace.shared.open(url) - #elseif os(iOS) - UIApplication.shared.open(url) - #endif - } - } - decisionHandler(.cancel) - } else { - decisionHandler(.cancel) - } - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - webView.evaluateJavaScript("expandAllDetailTags(); getOutlineItems();") - #if os(iOS) - webView.adjustTextSize() - #endif - } - - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - let error = error as NSError - guard error.code != NSURLErrorCancelled else { return } - NotificationCenter.default.post( - name: .alert, object: nil, userInfo: ["rawValue": ActiveAlert.articleFailedToLoad.rawValue] - ) - } - - // MARK: - WKScriptMessageHandler - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if message.name == "headings", let headings = message.body as? [[String: String]] { - DispatchQueue.global(qos: .userInitiated).async { - self.generateOutlineList(headings: headings) - self.generateOutlineTree(headings: headings) - } - } - } - - // MARK: - WKUIDelegate -#if os(macOS) - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, - for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { - - guard navigationAction.targetFrame == nil else { return nil } - guard let newUrl = navigationAction.request.url else { return nil } - - // open external link in default browser - guard newUrl.isExternal == false else { - externalURL = newUrl - return nil - } - - // create new tab - guard let currentWindow = NSApp.keyWindow, - let windowController = currentWindow.windowController else { return nil } - // store the new url in a static way - Self.urlForNewTab = newUrl - // this creates a new BrowserViewModel - windowController.newWindowForTab(self) - // now reset the static url to nil, as the new BrowserViewModel already has it - Self.urlForNewTab = nil - guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { return nil } - currentWindow.addTabbedWindow(newWindow, ordered: .above) - return nil - } -#endif - - #if os(iOS) - func webView(_ webView: WKWebView, - contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, - completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { - guard let url = elementInfo.linkURL, url.isKiwixURL else { completionHandler(nil); return } - let configuration = UIContextMenuConfiguration( - previewProvider: { - let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) - webView.load(URLRequest(url: url)) - return WebViewController(webView: webView) - }, actionProvider: { suggestedActions in - var actions = [UIAction]() - - // open url - actions.append( - UIAction(title: "Open", image: UIImage(systemName: "doc.text")) { _ in - webView.load(URLRequest(url: url)) - } - ) - actions.append( - UIAction(title: "Open in New Tab", image: UIImage(systemName: "doc.badge.plus")) { _ in - 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 { - return UIAction(title: "Remove Bookmark", image: UIImage(systemName: "star.slash.fill")) { _ in - self.deleteBookmark(url: url) - } - } else { - return UIAction(title: "Bookmark", image: UIImage(systemName: "star")) { _ in - self.createBookmark(url: url) - } - } - }() - actions.append(bookmarkAction) - - return UIMenu(children: actions) - } - ) - completionHandler(configuration) - } - #endif - // MARK: - Bookmark func controller(_ controller: NSFetchedResultsController, @@ -362,67 +235,4 @@ final class BrowserViewModel: NSObject, ObservableObject, func scrollTo(outlineItemID: String) { webView.evaluateJavaScript("scrollToHeading('\(outlineItemID)')") } - - /// Convert flattened heading element data to a list of OutlineItems. - /// - Parameter headings: list of heading element data retrieved from webview - private func generateOutlineList(headings: [[String: String]]) { - let allLevels = headings.compactMap { Int($0["tag"]?.suffix(1) ?? "") } - let offset = allLevels.filter({ $0 == 1 }).count == 1 ? 2 : allLevels.min() ?? 0 - let outlineItems: [OutlineItem] = headings.enumerated().compactMap { index, heading in - guard let id = heading["id"], - let text = heading["text"], - let tag = heading["tag"], - let level = Int(tag.suffix(1)) else { return nil } - return OutlineItem(id: id, index: index, text: text, level: max(level - offset, 0)) - } - DispatchQueue.main.async { - self.outlineItems = outlineItems - } - } - - /// Convert flattened heading element data to a tree of OutlineItems. - /// - Parameter headings: list of heading element data retrieved from webview - private func generateOutlineTree(headings: [[String: String]]) { - let root = OutlineItem(index: -1, text: "", level: 0) - var stack: [OutlineItem] = [root] - var all = [String: OutlineItem]() - - headings.enumerated().forEach { index, heading in - guard let id = heading["id"], - let text = heading["text"], - let tag = heading["tag"], let level = Int(tag.suffix(1)) else { return } - let item = OutlineItem(id: id, index: index, text: text, level: level) - all[item.id] = item - - // get last item in stack - // if last item is child of item's sibling, unwind stack until a sibling is found - guard var lastItem = stack.last else { return } - while lastItem.level > item.level { - stack.removeLast() - lastItem = stack[stack.count - 1] - } - - // if item is last item's sibling, add item to parent and replace last item with itself in stack - // if item is last item's child, add item to parent and add item to stack - if lastItem.level == item.level { - stack[stack.count - 2].addChild(item) - stack[stack.count - 1] = item - } else if lastItem.level < item.level { - stack[stack.count - 1].addChild(item) - stack.append(item) - } - } - - // if there is only one h1, flatten one level - if let rootChildren = root.children, rootChildren.count == 1, let rootFirstChild = rootChildren.first { - let children = rootFirstChild.removeAllChildren() - DispatchQueue.main.async { - self.outlineItemTree = [rootFirstChild] + children - } - } else { - DispatchQueue.main.async { - self.outlineItemTree = root.children ?? [] - } - } - } } From 21b33a1c76c17398019ed7a02a514f12094599cf Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 09:52:07 +0100 Subject: [PATCH 23/28] Format --- ViewModel/BrowserUIDelegate.swift | 136 +++++++++++++++--------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/ViewModel/BrowserUIDelegate.swift b/ViewModel/BrowserUIDelegate.swift index 0953382a..1cfc6128 100644 --- a/ViewModel/BrowserUIDelegate.swift +++ b/ViewModel/BrowserUIDelegate.swift @@ -8,83 +8,83 @@ import WebKit final class BrowserUIDelegate: NSObject, WKUIDelegate { - @Published private(set) var externalURL: URL? -#if os(macOS) - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, - for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + #if os(macOS) + func webView(_: WKWebView, createWebViewWith _: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures) -> WKWebView? + { + guard navigationAction.targetFrame == nil else { return nil } + guard let newUrl = navigationAction.request.url else { return nil } - guard navigationAction.targetFrame == nil else { return nil } - guard let newUrl = navigationAction.request.url else { return nil } + // open external link in default browser + guard newUrl.isExternal == false else { + externalURL = newUrl + return nil + } - // open external link in default browser - guard newUrl.isExternal == false else { - externalURL = newUrl + // create new tab + guard let currentWindow = NSApp.keyWindow, + let windowController = currentWindow.windowController else { return nil } + // store the new url in a static way + BrowserViewModel.urlForNewTab = newUrl + // this creates a new BrowserViewModel + windowController.newWindowForTab(self) + // now reset the static url to nil, as the new BrowserViewModel already has it + BrowserViewModel.urlForNewTab = nil + guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { return nil } + currentWindow.addTabbedWindow(newWindow, ordered: .above) return nil } + #endif - // create new tab - guard let currentWindow = NSApp.keyWindow, - let windowController = currentWindow.windowController else { return nil } - // store the new url in a static way - BrowserViewModel.urlForNewTab = newUrl - // this creates a new BrowserViewModel - windowController.newWindowForTab(self) - // now reset the static url to nil, as the new BrowserViewModel already has it - BrowserViewModel.urlForNewTab = nil - guard let newWindow = NSApp.keyWindow, currentWindow != newWindow else { return nil } - currentWindow.addTabbedWindow(newWindow, ordered: .above) - return nil - } -#endif + #if os(iOS) + func webView(_ webView: WKWebView, + contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, + completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) + { + guard let url = elementInfo.linkURL, url.isKiwixURL else { completionHandler(nil); return } + let configuration = UIContextMenuConfiguration( + previewProvider: { + let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) + webView.load(URLRequest(url: url)) + return WebViewController(webView: webView) + }, actionProvider: { _ in + var actions = [UIAction]() -#if os(iOS) - func webView(_ webView: WKWebView, - contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, - completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { - guard let url = elementInfo.linkURL, url.isKiwixURL else { completionHandler(nil); return } - let configuration = UIContextMenuConfiguration( - previewProvider: { - let webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) - webView.load(URLRequest(url: url)) - return WebViewController(webView: webView) - }, actionProvider: { suggestedActions in - var actions = [UIAction]() - - // open url - actions.append( - UIAction(title: "Open", image: UIImage(systemName: "doc.text")) { _ in - webView.load(URLRequest(url: url)) - } - ) - actions.append( - UIAction(title: "Open in New Tab", image: UIImage(systemName: "doc.badge.plus")) { _ in - 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 { - return UIAction(title: "Remove Bookmark", image: UIImage(systemName: "star.slash.fill")) { _ in - self.deleteBookmark(url: url) + // open url + actions.append( + UIAction(title: "Open", image: UIImage(systemName: "doc.text")) { _ in + webView.load(URLRequest(url: url)) } - } else { - return UIAction(title: "Bookmark", image: UIImage(systemName: "star")) { _ in - self.createBookmark(url: url) + ) + actions.append( + UIAction(title: "Open in New Tab", image: UIImage(systemName: "doc.badge.plus")) { _ in + NotificationCenter.openURL(url, inNewTab: true) } - } - }() - actions.append(bookmarkAction) + ) - return UIMenu(children: actions) - } - ) - completionHandler(configuration) - } -#endif + // 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 { + return UIAction(title: "Remove Bookmark", image: UIImage(systemName: "star.slash.fill")) { _ in + self.deleteBookmark(url: url) + } + } else { + return UIAction(title: "Bookmark", image: UIImage(systemName: "star")) { _ in + self.createBookmark(url: url) + } + } + }() + actions.append(bookmarkAction) + + return UIMenu(children: actions) + } + ) + completionHandler(configuration) + } + #endif } From bc9f59238d3152389e73afe3bceacebb2b6b428a Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 09:54:57 +0100 Subject: [PATCH 24/28] Format --- ViewModel/BrowserNavDelegate.swift | 16 ++++---- ViewModel/BrowserScriptHandler.swift | 4 +- ViewModel/BrowserViewModel.swift | 60 ++++++++++++++-------------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/ViewModel/BrowserNavDelegate.swift b/ViewModel/BrowserNavDelegate.swift index 70c323a2..527948cf 100644 --- a/ViewModel/BrowserNavDelegate.swift +++ b/ViewModel/BrowserNavDelegate.swift @@ -5,16 +5,16 @@ // Copyright © 2023 Chris Li. All rights reserved. // -import WebKit import CoreLocation +import WebKit final class BrowserNavDelegate: NSObject, WKNavigationDelegate { - @Published private(set) var externalURL: URL? func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) + { guard let url = navigationAction.request.url else { decisionHandler(.cancel); return } if url.isKiwixURL, let redirectedURL = ZimFileService.shared.getRedirectedURL(url: url) { DispatchQueue.main.async { webView.load(URLRequest(url: redirectedURL)) } @@ -38,9 +38,9 @@ final class BrowserNavDelegate: NSObject, WKNavigationDelegate { let coordinate = url.absoluteString.replacingOccurrences(of: "geo:", with: "") if let url = URL(string: "http://maps.apple.com/?ll=\(coordinate)") { #if os(macOS) - NSWorkspace.shared.open(url) + NSWorkspace.shared.open(url) #elseif os(iOS) - UIApplication.shared.open(url) + UIApplication.shared.open(url) #endif } } @@ -50,14 +50,14 @@ final class BrowserNavDelegate: NSObject, WKNavigationDelegate { } } - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { webView.evaluateJavaScript("expandAllDetailTags(); getOutlineItems();") #if os(iOS) - webView.adjustTextSize() + webView.adjustTextSize() #endif } - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError error: Error) { let error = error as NSError guard error.code != NSURLErrorCancelled else { return } NotificationCenter.default.post( diff --git a/ViewModel/BrowserScriptHandler.swift b/ViewModel/BrowserScriptHandler.swift index 696abaf4..1a2230a5 100644 --- a/ViewModel/BrowserScriptHandler.swift +++ b/ViewModel/BrowserScriptHandler.swift @@ -11,7 +11,7 @@ final class BrowserScriptHandler: NSObject, WKScriptMessageHandler { @Published private(set) var outlineItems = [OutlineItem]() @Published private(set) var outlineItemTree = [OutlineItem]() - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "headings", let headings = message.body as? [[String: String]] { DispatchQueue.global(qos: .userInitiated).async { self.generateOutlineList(headings: headings) @@ -24,7 +24,7 @@ final class BrowserScriptHandler: NSObject, WKScriptMessageHandler { /// - Parameter headings: list of heading element data retrieved from webview private func generateOutlineList(headings: [[String: String]]) { let allLevels = headings.compactMap { Int($0["tag"]?.suffix(1) ?? "") } - let offset = allLevels.filter({ $0 == 1 }).count == 1 ? 2 : allLevels.min() ?? 0 + let offset = allLevels.filter { $0 == 1 }.count == 1 ? 2 : allLevels.min() ?? 0 let outlineItems: [OutlineItem] = headings.enumerated().compactMap { index, heading in guard let id = heading["id"], let text = heading["text"], diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 5aeb0b8a..a1aae6c7 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -14,17 +14,17 @@ import WebKit import OrderedCollections final class BrowserViewModel: NSObject, ObservableObject, - NSFetchedResultsControllerDelegate + NSFetchedResultsControllerDelegate { - static private var cache = OrderedDictionary() - + private static var cache = OrderedDictionary() + static func getCached(tabID: NSManagedObjectID) -> BrowserViewModel { let viewModel = cache[tabID] ?? BrowserViewModel(tabID: tabID) cache.removeValue(forKey: tabID) cache[tabID] = viewModel return viewModel } - + static func purgeCache() { guard cache.count > 10 else { return } let range = 0 ..< cache.count - 5 @@ -33,9 +33,9 @@ final class BrowserViewModel: NSObject, ObservableObject, } cache.removeSubrange(range) } - + // MARK: - Properties - + @Published private(set) var canGoBack = false @Published private(set) var canGoForward = false @Published private(set) var articleTitle: String = "" @@ -60,10 +60,10 @@ final class BrowserViewModel: NSObject, ObservableObject, private var cancellables: Set = [] // MARK: - Lifecycle - + init(tabID: NSManagedObjectID? = nil) { self.tabID = tabID - self.webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) + webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) scriptHandler = BrowserScriptHandler() navDelegate = BrowserNavDelegate() uiDelegate = BrowserUIDelegate() @@ -93,7 +93,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // configure web view webView.allowsBackForwardNavigationGestures = true - webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile // for font adjustment to work + webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile // for font adjustment to work webView.configuration.userContentController.removeScriptMessageHandler(forName: "headings") webView.configuration.userContentController.add(scriptHandler, name: "headings") webView.navigationDelegate = navDelegate @@ -103,7 +103,7 @@ final class BrowserViewModel: NSObject, ObservableObject, if webView.url != nil { webView.evaluateJavaScript("getOutlineItems();") } - + // setup web view property observers canGoBackObserver = webView.observe(\.canGoBack, options: .initial) { [weak self] webView, _ in self?.canGoBack = webView.canGoBack @@ -129,24 +129,25 @@ final class BrowserViewModel: NSObject, ObservableObject, guard let url, let zimFileID = UUID(uuidString: url.host ?? "") else { return nil } return try? Database.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first }() - + // update view model self?.articleTitle = title ?? "" self?.zimFileName = zimFile?.name ?? "" self?.url = url - + // update tab data if let tabID = self?.tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab, - let title { + let title + { tab.title = title tab.zimFile = zimFile } - + // setup bookmark fetched results controller self?.bookmarkFetchedResultsController = NSFetchedResultsController( fetchRequest: Bookmark.fetchRequest(predicate: { - if let url = url { + if let url { return NSPredicate(format: "articleURL == %@", url as CVarArg) } else { return NSPredicate(format: "articleURL == nil") @@ -160,44 +161,45 @@ final class BrowserViewModel: NSObject, ObservableObject, try? self?.bookmarkFetchedResultsController?.performFetch() } } - + func updateLastOpened() { guard let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else { return } tab.lastOpened = Date() } - + func persistState() { guard let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else { return } tab.interactionState = webView.interactionState as? Data try? Database.viewContext.save() } - + // MARK: - Content Loading - + func load(url: URL) { guard webView.url != url else { return } webView.load(URLRequest(url: url)) } - + func loadRandomArticle(zimFileID: UUID? = nil) { let zimFileID = zimFileID ?? UUID(uuidString: webView.url?.host ?? "") guard let url = ZimFileService.shared.getRandomPageURL(zimFileID: zimFileID) else { return } load(url: url) } - + func loadMainArticle(zimFileID: UUID? = nil) { let zimFileID = zimFileID ?? UUID(uuidString: webView.url?.host ?? "") guard let url = ZimFileService.shared.getMainPageURL(zimFileID: zimFileID) else { return } load(url: url) } - + // MARK: - Bookmark - - func controller(_ controller: NSFetchedResultsController, - didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + + func controller(_: NSFetchedResultsController, + didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) + { articleBookmarked = !snapshot.itemIdentifiers.isEmpty } - + func createBookmark(url: URL? = nil) { guard let url = url ?? webView.url else { return } Database.performBackgroundTask { context in @@ -217,7 +219,7 @@ final class BrowserViewModel: NSObject, ObservableObject, try? context.save() } } - + func deleteBookmark(url: URL? = nil) { guard let url = url ?? webView.url else { return } Database.performBackgroundTask { context in @@ -227,9 +229,9 @@ final class BrowserViewModel: NSObject, ObservableObject, try? context.save() } } - + // MARK: - Outline - + /// Scroll to an outline item /// - Parameter outlineItemID: ID of the outline item to scroll to func scrollTo(outlineItemID: String) { From 97a3940ce072c5f2e545c9a6b115ecb5ebccb0cc Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 10:37:17 +0100 Subject: [PATCH 25/28] Fix format --- ViewModel/BrowserUIDelegate.swift | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ViewModel/BrowserUIDelegate.swift b/ViewModel/BrowserUIDelegate.swift index 1cfc6128..4c520956 100644 --- a/ViewModel/BrowserUIDelegate.swift +++ b/ViewModel/BrowserUIDelegate.swift @@ -11,9 +11,12 @@ final class BrowserUIDelegate: NSObject, WKUIDelegate { @Published private(set) var externalURL: URL? #if os(macOS) - func webView(_: WKWebView, createWebViewWith _: WKWebViewConfiguration, - for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures) -> WKWebView? - { + func webView( + _: WKWebView, + createWebViewWith _: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures _: WKWindowFeatures + ) -> WKWebView? { guard navigationAction.targetFrame == nil else { return nil } guard let newUrl = navigationAction.request.url else { return nil } @@ -39,10 +42,11 @@ final class BrowserUIDelegate: NSObject, WKUIDelegate { #endif #if os(iOS) - func webView(_ webView: WKWebView, - contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, - completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) - { + func webView( + _ webView: WKWebView, + contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, + completionHandler: @escaping (UIContextMenuConfiguration?) -> Void + ) { guard let url = elementInfo.linkURL, url.isKiwixURL else { completionHandler(nil); return } let configuration = UIContextMenuConfiguration( previewProvider: { @@ -70,7 +74,9 @@ final class BrowserUIDelegate: NSObject, WKUIDelegate { let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg) let request = Bookmark.fetchRequest(predicate: predicate) if let bookmarks = try? context.fetch(request), !bookmarks.isEmpty { - return UIAction(title: "Remove Bookmark", image: UIImage(systemName: "star.slash.fill")) { _ in + return UIAction(title: "Remove Bookmark", + image: UIImage(systemName: "star.slash.fill")) + { _ in self.deleteBookmark(url: url) } } else { From 0db61a180e4621e9d41582f7d1a50bd23b1d6393 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 10:54:33 +0100 Subject: [PATCH 26/28] Format --- ViewModel/BrowserNavDelegate.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ViewModel/BrowserNavDelegate.swift b/ViewModel/BrowserNavDelegate.swift index 527948cf..a1740ee1 100644 --- a/ViewModel/BrowserNavDelegate.swift +++ b/ViewModel/BrowserNavDelegate.swift @@ -11,10 +11,11 @@ import WebKit final class BrowserNavDelegate: NSObject, WKNavigationDelegate { @Published private(set) var externalURL: URL? - func webView(_ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) - { + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { guard let url = navigationAction.request.url else { decisionHandler(.cancel); return } if url.isKiwixURL, let redirectedURL = ZimFileService.shared.getRedirectedURL(url: url) { DispatchQueue.main.async { webView.load(URLRequest(url: redirectedURL)) } @@ -57,7 +58,11 @@ final class BrowserNavDelegate: NSObject, WKNavigationDelegate { #endif } - func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError error: Error) { + func webView( + _: WKWebView, + didFailProvisionalNavigation _: WKNavigation!, + withError error: Error + ) { let error = error as NSError guard error.code != NSURLErrorCancelled else { return } NotificationCenter.default.post( From 7b4e574d0fc8ceed8188c2e18b8534f502dc80be Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 11:02:35 +0100 Subject: [PATCH 27/28] Reformat --- ViewModel/BrowserUIDelegate.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ViewModel/BrowserUIDelegate.swift b/ViewModel/BrowserUIDelegate.swift index 4c520956..f1c5cb90 100644 --- a/ViewModel/BrowserUIDelegate.swift +++ b/ViewModel/BrowserUIDelegate.swift @@ -75,8 +75,7 @@ final class BrowserUIDelegate: NSObject, WKUIDelegate { let request = Bookmark.fetchRequest(predicate: predicate) if let bookmarks = try? context.fetch(request), !bookmarks.isEmpty { return UIAction(title: "Remove Bookmark", - image: UIImage(systemName: "star.slash.fill")) - { _ in + image: UIImage(systemName: "star.slash.fill")) { _ in self.deleteBookmark(url: url) } } else { From d489a2f591fbbe21dd2364be8e12d8875929c1af Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Tue, 14 Nov 2023 11:05:20 +0100 Subject: [PATCH 28/28] Reformat ViewModel --- ViewModel/BrowserViewModel.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index a1aae6c7..8a8a9c7b 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -138,8 +138,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // update tab data if let tabID = self?.tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab, - let title - { + let title { tab.title = title tab.zimFile = zimFile } @@ -195,8 +194,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // MARK: - Bookmark func controller(_: NSFetchedResultsController, - didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) - { + didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { articleBookmarked = !snapshot.itemIdentifiers.isEmpty }