Merge pull request #1134 from kiwix/live-activities-v2

Live activities
This commit is contained in:
Kelson 2025-03-07 14:56:22 +01:00 committed by GitHub
commit 31c4d5214b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 105 additions and 64 deletions

View File

@ -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") {
@ -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 {
@ -85,7 +92,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()

View File

@ -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)
}
}
}
}
}

View File

@ -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,17 @@ 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 {
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

View File

@ -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

View File

@ -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) {

View File

@ -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<Void, Never>()
#if os(macOS)
var isTerminating: Bool = false

View File

@ -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,10 @@ struct Library: View {
viewModel.start(isUserInitiated: false)
}.onDisappear {
hasSeenCategories = true
}.onReceive(navigation.showDownloads) { _ in
if tabItem != .downloads {
tabItem = .downloads
}
}
}
}

View File

@ -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)

View File

@ -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)
@ -44,19 +36,15 @@ struct DownloadsLiveActivity: Widget {
}
}
.modifier(WidgetBackgroundModifier())
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
} dynamicIsland: { context in
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 +67,7 @@ struct DownloadsLiveActivity: Widget {
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
.frame(width: 24, height: 24)
}
.widgetURL(URL(string: "https://www.kiwix.org"))
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
.keylineTint(Color.red)
}.containerBackgroundRemovable()
}
@ -101,11 +89,25 @@ struct DownloadsLiveActivity: Widget {
.tint(.secondary)
}
private func currentTimeInterval(
state: DownloadActivityAttributes.ContentState
) -> ClosedRange<Date> {
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<Date>
state: DownloadActivityAttributes.ContentState
) -> some View {
let timeInterval = currentTimeInterval(state: state)
if !state.isAllPaused {
ProgressView(timerInterval: timeInterval, countsDown: false, label: {
progressText(state.progressDescription)
@ -146,14 +148,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 +170,7 @@ extension DownloadActivityAttributes.ContentState {
description: "2nd item",
downloaded: 110,
total: 124,
timeRemaining: 2,
timeRemaining: 20,
isPaused: false
)
]

View File

@ -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: