From 4aa465e87880956f35f0dddea50cadda7a652464 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 01:41:07 +0100 Subject: [PATCH 1/7] Revert "Remove live activities from release 3.8.0" This reverts commit 659d8ee6aa042114749173d9153711c92a57178b. --- App/App_iOS.swift | 16 ++++++++-------- CHANGELOG.md | 2 ++ project.yml | 34 +++++++++++++++++----------------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 3436932f..1a06d9bd 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -25,17 +25,17 @@ struct Kiwix: App { @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate private let fileMonitor: DirectoryMonitor -// private let activityService: ActivityService? + private let activityService: ActivityService? init() { fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) } // MARK: - live activities -// switch AppType.current { -// case .kiwix: -// activityService = ActivityService() -// case .custom: -// activityService = nil -// } + switch AppType.current { + case .kiwix: + activityService = ActivityService() + case .custom: + activityService = nil + } UNUserNotificationCenter.current().delegate = appDelegate // MARK: - migrations if !ProcessInfo.processInfo.arguments.contains("testing") { @@ -85,7 +85,7 @@ struct Kiwix: App { LibraryOperations.scanDirectory(URL.documentDirectory) LibraryOperations.applyFileBackupSetting() DownloadService.shared.restartHeartbeatIfNeeded() -// activityService?.start() + activityService?.start() case let .custom(zimFileURL): await LibraryOperations.open(url: zimFileURL) ZimMigration.forCustomApps() diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e1ed6d..b3ad2bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # 3.9.0 # 3.8.0 + - NEW: + - Live activities for downloads (@BPerlakiH #1096, #1105, #1106, #1114, #1126) - UPDATE: - Localisations (@translatewiki #1095, #1102, #1108) - Keyboard navigation improvements for macOS (@BPerlakiH #1084) diff --git a/project.yml b/project.yml index d4cbf432..abfc69cb 100644 --- a/project.yml +++ b/project.yml @@ -121,10 +121,10 @@ targets: - path: Kiwix/SplashScreenKiwix.storyboard destinationFilters: - iOS - # dependencies: - # - target: Widgets - # destinationFilters: - # - iOS + dependencies: + - target: Widgets + destinationFilters: + - iOS UnitTests: type: bundle.unit-test supportedDestinations: [iOS, macOS] @@ -143,19 +143,19 @@ targets: - path: Tests dependencies: - target: Kiwix - # Widgets: - # type: app-extension - # supportedDestinations: [iOS] - # settings: - # base: - # PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.ioswidgets - # INFOPLIST_FILE: Widgets/Info.plist - # sources: - # - path: Common - # - path: Widgets - # dependencies: - # - framework: SwiftUI.framework - # - framework: WidgetKit.framework + Widgets: + type: app-extension + supportedDestinations: [iOS] + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.ioswidgets + INFOPLIST_FILE: Widgets/Info.plist + sources: + - path: Common + - path: Widgets + dependencies: + - framework: SwiftUI.framework + - framework: WidgetKit.framework schemes: Kiwix: From 5b5cce7d41631683b451e2b8672a885a9aa0e02b Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 01:41:44 +0100 Subject: [PATCH 2/7] Revert CHANGELOG --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ad2bc9..68e1ed6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # 3.9.0 # 3.8.0 - - NEW: - - Live activities for downloads (@BPerlakiH #1096, #1105, #1106, #1114, #1126) - UPDATE: - Localisations (@translatewiki #1095, #1102, #1108) - Keyboard navigation improvements for macOS (@BPerlakiH #1084) From f2e4b9ad45b1fd7cb15d67aa6d99d3f427bf1d88 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 09:30:48 +0100 Subject: [PATCH 3/7] Fix time passed calculation --- Widgets/DownloadsLiveActivity.swift | 47 ++++++++++++----------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/Widgets/DownloadsLiveActivity.swift b/Widgets/DownloadsLiveActivity.swift index 4eaca6f5..e3aee799 100644 --- a/Widgets/DownloadsLiveActivity.swift +++ b/Widgets/DownloadsLiveActivity.swift @@ -21,22 +21,14 @@ struct DownloadsLiveActivity: Widget { // @Environment(\.isActivityFullscreen) var isActivityFullScreen has a bug, when min iOS is 16 // https://developer.apple.com/forums/thread/763594 - /// A start time from the creation of the activity, - /// this way the progress bar is not jumping back to 0 - private let startTime: Date = .now - var body: some WidgetConfiguration { ActivityConfiguration(for: DownloadActivityAttributes.self) { context in // Lock screen/banner UI - let timeInterval = startTime...Date( - timeInterval: context.state.estimatedTimeLeft, - since: .now - ) VStack { HStack { VStack(alignment: .leading) { titleFor(context.state.title) - progressFor(state: context.state, timeInterval: timeInterval) + progressFor(state: context.state) } .padding() KiwixLogo(maxHeight: 50) @@ -49,14 +41,9 @@ struct DownloadsLiveActivity: Widget { DynamicIsland { // Expanded UI DynamicIslandExpandedRegion(.leading) { - let timeInterval = startTime...Date( - timeInterval: context.state.estimatedTimeLeft, - since: .now - ) - VStack(alignment: .leading) { titleFor(context.state.title) - progressFor(state: context.state, timeInterval: timeInterval) + progressFor(state: context.state) Spacer() } .padding() @@ -79,7 +66,7 @@ struct DownloadsLiveActivity: Widget { .progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7)) .frame(width: 24, height: 24) } - .widgetURL(URL(string: "https://www.kiwix.org")) + .widgetURL(URL(string: "zim://downloads")) .keylineTint(Color.red) }.containerBackgroundRemovable() } @@ -101,11 +88,25 @@ struct DownloadsLiveActivity: Widget { .tint(.secondary) } + private func currentTimeInterval( + state: DownloadActivityAttributes.ContentState + ) -> ClosedRange { + if state.progress < 1 { + let timePassed: TimeInterval = state.progress / (1 - state.progress) * state.estimatedTimeLeft + return Date(timeInterval: 0 - timePassed, since: .now)...Date( + timeInterval: state.estimatedTimeLeft, + since: .now + ) + } else { + return Date(timeIntervalSinceNow: 0)...Date(timeIntervalSinceNow: 0) + } + } + @ViewBuilder private func progressFor( - state: DownloadActivityAttributes.ContentState, - timeInterval: ClosedRange + state: DownloadActivityAttributes.ContentState ) -> some View { + let timeInterval = currentTimeInterval(state: state) if !state.isAllPaused { ProgressView(timerInterval: timeInterval, countsDown: false, label: { progressText(state.progressDescription) @@ -146,14 +147,6 @@ extension DownloadActivityAttributes.ContentState { total: 256, timeRemaining: 15, isPaused: true - ), - DownloadActivityAttributes.DownloadItem( - uuid: UUID(), - description: "2nd item", - downloaded: 90, - total: 124, - timeRemaining: 2, - isPaused: true ) ] ) @@ -176,7 +169,7 @@ extension DownloadActivityAttributes.ContentState { description: "2nd item", downloaded: 110, total: 124, - timeRemaining: 2, + timeRemaining: 20, isPaused: false ) ] From d057f95ec0198ec3383c807b540e59c4385e807a Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 09:44:22 +0100 Subject: [PATCH 4/7] Fix typo start with more frequent updates --- Model/Utilities/DownloadTime.swift | 6 +++--- Views/LiveActivity/ActivityService.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Model/Utilities/DownloadTime.swift b/Model/Utilities/DownloadTime.swift index b5e3290a..02f44ae9 100644 --- a/Model/Utilities/DownloadTime.swift +++ b/Model/Utilities/DownloadTime.swift @@ -43,11 +43,11 @@ final class DownloadTime { } let average = averagePerSecond() let remainingAmount = totalAmount - latestAmount - let remaingTime = Double(remainingAmount) / average - (now - latestTime) - guard remaingTime > 0 else { + let remainingTime = Double(remainingAmount) / average - (now - latestTime) + guard remainingTime > 0 else { return 0 } - return remaingTime + return remainingTime } private func filterOutSamples(now: CFTimeInterval) { diff --git a/Views/LiveActivity/ActivityService.swift b/Views/LiveActivity/ActivityService.swift index 9f8e2354..741e6ca9 100644 --- a/Views/LiveActivity/ActivityService.swift +++ b/Views/LiveActivity/ActivityService.swift @@ -35,7 +35,7 @@ final class ActivityService { publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never> = { DownloadService.shared.progress.publisher }, - updateFrequency: Double = 10, + updateFrequency: Double = 2, averageDownloadSpeedFromLastSeconds: Double = 30 ) { assert(updateFrequency > 0) From a98cc5cace2af03bf63b0b4ef18ff27745cc8341 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 18:22:49 +0100 Subject: [PATCH 5/7] Navigate to downloads via deeplink --- App/App_iOS.swift | 9 +++++++- App/CompactViewController.swift | 28 +++++++++++++++++++------ App/SplitViewController.swift | 11 ++++++++++ Common/DownloadActivityAttributes.swift | 2 ++ ViewModel/NavigationViewModel.swift | 3 +++ Views/Library/Library.swift | 7 ++++++- Widgets/DownloadsLiveActivity.swift | 2 +- 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 1a06d9bd..c0a946e5 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -73,7 +73,14 @@ struct Kiwix: App { if url.isFileURL { NotificationCenter.openFiles([url], context: .file) } else if url.isZIMURL { - NotificationCenter.openURL(url) + switch url { + case DownloadActivityAttributes.downloadsDeepLink: + if FeatureFlags.hasLibrary { + navigation.showDownloads.send() + } + default: + NotificationCenter.openURL(url) + } } } .task { diff --git a/App/CompactViewController.swift b/App/CompactViewController.swift index d64003d5..f301aaa7 100644 --- a/App/CompactViewController.swift +++ b/App/CompactViewController.swift @@ -137,11 +137,15 @@ private struct CompactView: View { @EnvironmentObject private var library: LibraryViewModel @State private var presentedSheet: PresentedSheet? - private enum PresentedSheet: String, Identifiable { - case library + private enum PresentedSheet: Identifiable { + case library(downloads: Bool) case settings var id: String { - rawValue + switch self { + case .library(true): return "library-downloads" + case .library(false): return "library" + case .settings: return "settings" + } } } @@ -161,7 +165,7 @@ private struct CompactView: View { } Content(tabID: tabID, showLibrary: { if presentedSheet == nil { - presentedSheet = .library + presentedSheet = .library(downloads: false) } else { // there's a sheet already presented by the user // do nothing @@ -183,7 +187,7 @@ private struct CompactView: View { Spacer() if FeatureFlags.hasLibrary { Button { - presentedSheet = .library + presentedSheet = .library(downloads: false) } label: { Label(LocalString.common_tab_menu_library, systemImage: "folder") } @@ -200,8 +204,10 @@ private struct CompactView: View { .environmentObject(browser) .sheet(item: $presentedSheet) { presentedSheet in switch presentedSheet { - case .library: + case .library(downloads: false): Library(dismiss: dismiss) + case .library(downloads: true): + Library(dismiss: dismiss, tabItem: .downloads) case .settings: NavigationStack { Settings().toolbar { @@ -216,6 +222,16 @@ private struct CompactView: View { } } } + .onReceive(navigation.showDownloads) { _ in + switch presentedSheet { + case .library: + // switching to the downloads tab + // is done within Library + break + case .settings, nil: + presentedSheet = .library(downloads: true) + } + } } } } diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index c13cfffe..8c134186 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -21,6 +21,7 @@ import UIKit final class SplitViewController: UISplitViewController { let navigationViewModel: NavigationViewModel private var navigationItemObserver: AnyCancellable? + private var showDownloadsObserver: AnyCancellable? private var openURLObserver: NSObjectProtocol? private var hasZimFiles: Bool @@ -74,6 +75,16 @@ final class SplitViewController: UISplitViewController { self?.preferredDisplayMode = .automatic } } + showDownloadsObserver = navigationViewModel + .showDownloads + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] _ in + if self?.traitCollection.horizontalSizeClass == .regular { + self?.navigationViewModel.currentItem = .downloads + } + // the compact one is triggered in CompactViewController + }) + openURLObserver = NotificationCenter.default.addObserver( forName: .openURL, object: nil, queue: nil ) { [weak self] notification in diff --git a/Common/DownloadActivityAttributes.swift b/Common/DownloadActivityAttributes.swift index e894c9b3..69e2ad18 100644 --- a/Common/DownloadActivityAttributes.swift +++ b/Common/DownloadActivityAttributes.swift @@ -18,6 +18,8 @@ import ActivityKit public struct DownloadActivityAttributes: ActivityAttributes { + static let downloadsDeepLink = URL(string: "zim://downloads") + private static func progressFor(items: [DownloadItem]) -> Progress { let sumOfTotal = items.reduce(0) { result, item in result + item.total diff --git a/ViewModel/NavigationViewModel.swift b/ViewModel/NavigationViewModel.swift index 37b88768..e4f9939e 100644 --- a/ViewModel/NavigationViewModel.swift +++ b/ViewModel/NavigationViewModel.swift @@ -15,12 +15,15 @@ import CoreData import WebKit +import Combine @MainActor final class NavigationViewModel: ObservableObject { let uuid = UUID() // remained optional due to focusedSceneValue conformance @Published var currentItem: NavigationItem? = .loading + private(set) var showDownloads = PassthroughSubject() + #if os(macOS) var isTerminating: Bool = false diff --git a/Views/Library/Library.swift b/Views/Library/Library.swift index 99e52acb..c999cb0c 100644 --- a/Views/Library/Library.swift +++ b/Views/Library/Library.swift @@ -21,16 +21,19 @@ import Defaults /// Tabbed library view on iOS & iPadOS struct Library: View { @EnvironmentObject private var viewModel: LibraryViewModel - @SceneStorage("LibraryTabItem") private var tabItem: LibraryTabItem = .categories + @EnvironmentObject private var navigation: NavigationViewModel + @State private var tabItem: LibraryTabItem @Default(.hasSeenCategories) private var hasSeenCategories private let categories: [Category] let dismiss: (() -> Void)? init( dismiss: (() -> Void)?, + tabItem: LibraryTabItem = .categories, categories: [Category] = CategoriesToLanguages().allCategories() ) { self.dismiss = dismiss + self.tabItem = tabItem self.categories = categories } @@ -70,6 +73,8 @@ struct Library: View { viewModel.start(isUserInitiated: false) }.onDisappear { hasSeenCategories = true + }.onReceive(navigation.showDownloads) { _ in + tabItem = .downloads } } } diff --git a/Widgets/DownloadsLiveActivity.swift b/Widgets/DownloadsLiveActivity.swift index e3aee799..1ce8dce5 100644 --- a/Widgets/DownloadsLiveActivity.swift +++ b/Widgets/DownloadsLiveActivity.swift @@ -66,7 +66,7 @@ struct DownloadsLiveActivity: Widget { .progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7)) .frame(width: 24, height: 24) } - .widgetURL(URL(string: "zim://downloads")) + .widgetURL(DownloadActivityAttributes.downloadsDeepLink) .keylineTint(Color.red) }.containerBackgroundRemovable() } From ac30110eeeaad51d9e6bd7f3311246928a68788d Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 18:29:23 +0100 Subject: [PATCH 6/7] Add downloads url to lock screen widget --- Widgets/DownloadsLiveActivity.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Widgets/DownloadsLiveActivity.swift b/Widgets/DownloadsLiveActivity.swift index 1ce8dce5..c735025e 100644 --- a/Widgets/DownloadsLiveActivity.swift +++ b/Widgets/DownloadsLiveActivity.swift @@ -36,6 +36,7 @@ struct DownloadsLiveActivity: Widget { } } .modifier(WidgetBackgroundModifier()) + .widgetURL(DownloadActivityAttributes.downloadsDeepLink) } dynamicIsland: { context in DynamicIsland { From 48aa306b1ab5bb379e0edf9cbdd67e67b61a2ce2 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 2 Mar 2025 20:51:24 +0100 Subject: [PATCH 7/7] Fix not to navigate away from download details --- App/SplitViewController.swift | 5 +++-- Views/Library/Library.swift | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 8c134186..df51efaa 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -79,8 +79,9 @@ final class SplitViewController: UISplitViewController { .showDownloads .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] _ in - if self?.traitCollection.horizontalSizeClass == .regular { - self?.navigationViewModel.currentItem = .downloads + if self?.traitCollection.horizontalSizeClass == .regular, + self?.navigationViewModel.currentItem != .downloads { + self?.navigationViewModel.currentItem = .downloads } // the compact one is triggered in CompactViewController }) diff --git a/Views/Library/Library.swift b/Views/Library/Library.swift index c999cb0c..39cc1619 100644 --- a/Views/Library/Library.swift +++ b/Views/Library/Library.swift @@ -74,7 +74,9 @@ struct Library: View { }.onDisappear { hasSeenCategories = true }.onReceive(navigation.showDownloads) { _ in - tabItem = .downloads + if tabItem != .downloads { + tabItem = .downloads + } } } }