mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-21 02:21:39 -04:00
commit
31c4d5214b
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
]
|
||||
|
34
project.yml
34
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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user