mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-22 11:03:21 -04:00
commit
31c4d5214b
@ -25,17 +25,17 @@ struct Kiwix: App {
|
|||||||
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||||
|
|
||||||
private let fileMonitor: DirectoryMonitor
|
private let fileMonitor: DirectoryMonitor
|
||||||
// private let activityService: ActivityService?
|
private let activityService: ActivityService?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) }
|
fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) }
|
||||||
// MARK: - live activities
|
// MARK: - live activities
|
||||||
// switch AppType.current {
|
switch AppType.current {
|
||||||
// case .kiwix:
|
case .kiwix:
|
||||||
// activityService = ActivityService()
|
activityService = ActivityService()
|
||||||
// case .custom:
|
case .custom:
|
||||||
// activityService = nil
|
activityService = nil
|
||||||
// }
|
}
|
||||||
UNUserNotificationCenter.current().delegate = appDelegate
|
UNUserNotificationCenter.current().delegate = appDelegate
|
||||||
// MARK: - migrations
|
// MARK: - migrations
|
||||||
if !ProcessInfo.processInfo.arguments.contains("testing") {
|
if !ProcessInfo.processInfo.arguments.contains("testing") {
|
||||||
@ -73,7 +73,14 @@ struct Kiwix: App {
|
|||||||
if url.isFileURL {
|
if url.isFileURL {
|
||||||
NotificationCenter.openFiles([url], context: .file)
|
NotificationCenter.openFiles([url], context: .file)
|
||||||
} else if url.isZIMURL {
|
} else if url.isZIMURL {
|
||||||
NotificationCenter.openURL(url)
|
switch url {
|
||||||
|
case DownloadActivityAttributes.downloadsDeepLink:
|
||||||
|
if FeatureFlags.hasLibrary {
|
||||||
|
navigation.showDownloads.send()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
NotificationCenter.openURL(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
@ -85,7 +92,7 @@ struct Kiwix: App {
|
|||||||
LibraryOperations.scanDirectory(URL.documentDirectory)
|
LibraryOperations.scanDirectory(URL.documentDirectory)
|
||||||
LibraryOperations.applyFileBackupSetting()
|
LibraryOperations.applyFileBackupSetting()
|
||||||
DownloadService.shared.restartHeartbeatIfNeeded()
|
DownloadService.shared.restartHeartbeatIfNeeded()
|
||||||
// activityService?.start()
|
activityService?.start()
|
||||||
case let .custom(zimFileURL):
|
case let .custom(zimFileURL):
|
||||||
await LibraryOperations.open(url: zimFileURL)
|
await LibraryOperations.open(url: zimFileURL)
|
||||||
ZimMigration.forCustomApps()
|
ZimMigration.forCustomApps()
|
||||||
|
@ -137,11 +137,15 @@ private struct CompactView: View {
|
|||||||
@EnvironmentObject private var library: LibraryViewModel
|
@EnvironmentObject private var library: LibraryViewModel
|
||||||
@State private var presentedSheet: PresentedSheet?
|
@State private var presentedSheet: PresentedSheet?
|
||||||
|
|
||||||
private enum PresentedSheet: String, Identifiable {
|
private enum PresentedSheet: Identifiable {
|
||||||
case library
|
case library(downloads: Bool)
|
||||||
case settings
|
case settings
|
||||||
var id: String {
|
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: {
|
Content(tabID: tabID, showLibrary: {
|
||||||
if presentedSheet == nil {
|
if presentedSheet == nil {
|
||||||
presentedSheet = .library
|
presentedSheet = .library(downloads: false)
|
||||||
} else {
|
} else {
|
||||||
// there's a sheet already presented by the user
|
// there's a sheet already presented by the user
|
||||||
// do nothing
|
// do nothing
|
||||||
@ -183,7 +187,7 @@ private struct CompactView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
if FeatureFlags.hasLibrary {
|
if FeatureFlags.hasLibrary {
|
||||||
Button {
|
Button {
|
||||||
presentedSheet = .library
|
presentedSheet = .library(downloads: false)
|
||||||
} label: {
|
} label: {
|
||||||
Label(LocalString.common_tab_menu_library, systemImage: "folder")
|
Label(LocalString.common_tab_menu_library, systemImage: "folder")
|
||||||
}
|
}
|
||||||
@ -200,8 +204,10 @@ private struct CompactView: View {
|
|||||||
.environmentObject(browser)
|
.environmentObject(browser)
|
||||||
.sheet(item: $presentedSheet) { presentedSheet in
|
.sheet(item: $presentedSheet) { presentedSheet in
|
||||||
switch presentedSheet {
|
switch presentedSheet {
|
||||||
case .library:
|
case .library(downloads: false):
|
||||||
Library(dismiss: dismiss)
|
Library(dismiss: dismiss)
|
||||||
|
case .library(downloads: true):
|
||||||
|
Library(dismiss: dismiss, tabItem: .downloads)
|
||||||
case .settings:
|
case .settings:
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Settings().toolbar {
|
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 {
|
final class SplitViewController: UISplitViewController {
|
||||||
let navigationViewModel: NavigationViewModel
|
let navigationViewModel: NavigationViewModel
|
||||||
private var navigationItemObserver: AnyCancellable?
|
private var navigationItemObserver: AnyCancellable?
|
||||||
|
private var showDownloadsObserver: AnyCancellable?
|
||||||
private var openURLObserver: NSObjectProtocol?
|
private var openURLObserver: NSObjectProtocol?
|
||||||
private var hasZimFiles: Bool
|
private var hasZimFiles: Bool
|
||||||
|
|
||||||
@ -74,6 +75,17 @@ final class SplitViewController: UISplitViewController {
|
|||||||
self?.preferredDisplayMode = .automatic
|
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(
|
openURLObserver = NotificationCenter.default.addObserver(
|
||||||
forName: .openURL, object: nil, queue: nil
|
forName: .openURL, object: nil, queue: nil
|
||||||
) { [weak self] notification in
|
) { [weak self] notification in
|
||||||
|
@ -18,6 +18,8 @@ import ActivityKit
|
|||||||
|
|
||||||
public struct DownloadActivityAttributes: ActivityAttributes {
|
public struct DownloadActivityAttributes: ActivityAttributes {
|
||||||
|
|
||||||
|
static let downloadsDeepLink = URL(string: "zim://downloads")
|
||||||
|
|
||||||
private static func progressFor(items: [DownloadItem]) -> Progress {
|
private static func progressFor(items: [DownloadItem]) -> Progress {
|
||||||
let sumOfTotal = items.reduce(0) { result, item in
|
let sumOfTotal = items.reduce(0) { result, item in
|
||||||
result + item.total
|
result + item.total
|
||||||
|
@ -43,11 +43,11 @@ final class DownloadTime {
|
|||||||
}
|
}
|
||||||
let average = averagePerSecond()
|
let average = averagePerSecond()
|
||||||
let remainingAmount = totalAmount - latestAmount
|
let remainingAmount = totalAmount - latestAmount
|
||||||
let remaingTime = Double(remainingAmount) / average - (now - latestTime)
|
let remainingTime = Double(remainingAmount) / average - (now - latestTime)
|
||||||
guard remaingTime > 0 else {
|
guard remainingTime > 0 else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return remaingTime
|
return remainingTime
|
||||||
}
|
}
|
||||||
|
|
||||||
private func filterOutSamples(now: CFTimeInterval) {
|
private func filterOutSamples(now: CFTimeInterval) {
|
||||||
|
@ -15,12 +15,15 @@
|
|||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import WebKit
|
import WebKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class NavigationViewModel: ObservableObject {
|
final class NavigationViewModel: ObservableObject {
|
||||||
let uuid = UUID()
|
let uuid = UUID()
|
||||||
// remained optional due to focusedSceneValue conformance
|
// remained optional due to focusedSceneValue conformance
|
||||||
@Published var currentItem: NavigationItem? = .loading
|
@Published var currentItem: NavigationItem? = .loading
|
||||||
|
private(set) var showDownloads = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
var isTerminating: Bool = false
|
var isTerminating: Bool = false
|
||||||
|
|
||||||
|
@ -21,16 +21,19 @@ import Defaults
|
|||||||
/// Tabbed library view on iOS & iPadOS
|
/// Tabbed library view on iOS & iPadOS
|
||||||
struct Library: View {
|
struct Library: View {
|
||||||
@EnvironmentObject private var viewModel: LibraryViewModel
|
@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
|
@Default(.hasSeenCategories) private var hasSeenCategories
|
||||||
private let categories: [Category]
|
private let categories: [Category]
|
||||||
let dismiss: (() -> Void)?
|
let dismiss: (() -> Void)?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
dismiss: (() -> Void)?,
|
dismiss: (() -> Void)?,
|
||||||
|
tabItem: LibraryTabItem = .categories,
|
||||||
categories: [Category] = CategoriesToLanguages().allCategories()
|
categories: [Category] = CategoriesToLanguages().allCategories()
|
||||||
) {
|
) {
|
||||||
self.dismiss = dismiss
|
self.dismiss = dismiss
|
||||||
|
self.tabItem = tabItem
|
||||||
self.categories = categories
|
self.categories = categories
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +73,10 @@ struct Library: View {
|
|||||||
viewModel.start(isUserInitiated: false)
|
viewModel.start(isUserInitiated: false)
|
||||||
}.onDisappear {
|
}.onDisappear {
|
||||||
hasSeenCategories = true
|
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> = {
|
publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never> = {
|
||||||
DownloadService.shared.progress.publisher
|
DownloadService.shared.progress.publisher
|
||||||
},
|
},
|
||||||
updateFrequency: Double = 10,
|
updateFrequency: Double = 2,
|
||||||
averageDownloadSpeedFromLastSeconds: Double = 30
|
averageDownloadSpeedFromLastSeconds: Double = 30
|
||||||
) {
|
) {
|
||||||
assert(updateFrequency > 0)
|
assert(updateFrequency > 0)
|
||||||
|
@ -21,22 +21,14 @@ struct DownloadsLiveActivity: Widget {
|
|||||||
// @Environment(\.isActivityFullscreen) var isActivityFullScreen has a bug, when min iOS is 16
|
// @Environment(\.isActivityFullscreen) var isActivityFullScreen has a bug, when min iOS is 16
|
||||||
// https://developer.apple.com/forums/thread/763594
|
// 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 {
|
var body: some WidgetConfiguration {
|
||||||
ActivityConfiguration(for: DownloadActivityAttributes.self) { context in
|
ActivityConfiguration(for: DownloadActivityAttributes.self) { context in
|
||||||
// Lock screen/banner UI
|
// Lock screen/banner UI
|
||||||
let timeInterval = startTime...Date(
|
|
||||||
timeInterval: context.state.estimatedTimeLeft,
|
|
||||||
since: .now
|
|
||||||
)
|
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
titleFor(context.state.title)
|
titleFor(context.state.title)
|
||||||
progressFor(state: context.state, timeInterval: timeInterval)
|
progressFor(state: context.state)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
KiwixLogo(maxHeight: 50)
|
KiwixLogo(maxHeight: 50)
|
||||||
@ -44,19 +36,15 @@ struct DownloadsLiveActivity: Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(WidgetBackgroundModifier())
|
.modifier(WidgetBackgroundModifier())
|
||||||
|
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
|
||||||
|
|
||||||
} dynamicIsland: { context in
|
} dynamicIsland: { context in
|
||||||
DynamicIsland {
|
DynamicIsland {
|
||||||
// Expanded UI
|
// Expanded UI
|
||||||
DynamicIslandExpandedRegion(.leading) {
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
let timeInterval = startTime...Date(
|
|
||||||
timeInterval: context.state.estimatedTimeLeft,
|
|
||||||
since: .now
|
|
||||||
)
|
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
titleFor(context.state.title)
|
titleFor(context.state.title)
|
||||||
progressFor(state: context.state, timeInterval: timeInterval)
|
progressFor(state: context.state)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@ -79,7 +67,7 @@ struct DownloadsLiveActivity: Widget {
|
|||||||
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
|
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
}
|
}
|
||||||
.widgetURL(URL(string: "https://www.kiwix.org"))
|
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
|
||||||
.keylineTint(Color.red)
|
.keylineTint(Color.red)
|
||||||
}.containerBackgroundRemovable()
|
}.containerBackgroundRemovable()
|
||||||
}
|
}
|
||||||
@ -101,11 +89,25 @@ struct DownloadsLiveActivity: Widget {
|
|||||||
.tint(.secondary)
|
.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
|
@ViewBuilder
|
||||||
private func progressFor(
|
private func progressFor(
|
||||||
state: DownloadActivityAttributes.ContentState,
|
state: DownloadActivityAttributes.ContentState
|
||||||
timeInterval: ClosedRange<Date>
|
|
||||||
) -> some View {
|
) -> some View {
|
||||||
|
let timeInterval = currentTimeInterval(state: state)
|
||||||
if !state.isAllPaused {
|
if !state.isAllPaused {
|
||||||
ProgressView(timerInterval: timeInterval, countsDown: false, label: {
|
ProgressView(timerInterval: timeInterval, countsDown: false, label: {
|
||||||
progressText(state.progressDescription)
|
progressText(state.progressDescription)
|
||||||
@ -146,14 +148,6 @@ extension DownloadActivityAttributes.ContentState {
|
|||||||
total: 256,
|
total: 256,
|
||||||
timeRemaining: 15,
|
timeRemaining: 15,
|
||||||
isPaused: true
|
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",
|
description: "2nd item",
|
||||||
downloaded: 110,
|
downloaded: 110,
|
||||||
total: 124,
|
total: 124,
|
||||||
timeRemaining: 2,
|
timeRemaining: 20,
|
||||||
isPaused: false
|
isPaused: false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
34
project.yml
34
project.yml
@ -121,10 +121,10 @@ targets:
|
|||||||
- path: Kiwix/SplashScreenKiwix.storyboard
|
- path: Kiwix/SplashScreenKiwix.storyboard
|
||||||
destinationFilters:
|
destinationFilters:
|
||||||
- iOS
|
- iOS
|
||||||
# dependencies:
|
dependencies:
|
||||||
# - target: Widgets
|
- target: Widgets
|
||||||
# destinationFilters:
|
destinationFilters:
|
||||||
# - iOS
|
- iOS
|
||||||
UnitTests:
|
UnitTests:
|
||||||
type: bundle.unit-test
|
type: bundle.unit-test
|
||||||
supportedDestinations: [iOS, macOS]
|
supportedDestinations: [iOS, macOS]
|
||||||
@ -143,19 +143,19 @@ targets:
|
|||||||
- path: Tests
|
- path: Tests
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: Kiwix
|
- target: Kiwix
|
||||||
# Widgets:
|
Widgets:
|
||||||
# type: app-extension
|
type: app-extension
|
||||||
# supportedDestinations: [iOS]
|
supportedDestinations: [iOS]
|
||||||
# settings:
|
settings:
|
||||||
# base:
|
base:
|
||||||
# PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.ioswidgets
|
PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.ioswidgets
|
||||||
# INFOPLIST_FILE: Widgets/Info.plist
|
INFOPLIST_FILE: Widgets/Info.plist
|
||||||
# sources:
|
sources:
|
||||||
# - path: Common
|
- path: Common
|
||||||
# - path: Widgets
|
- path: Widgets
|
||||||
# dependencies:
|
dependencies:
|
||||||
# - framework: SwiftUI.framework
|
- framework: SwiftUI.framework
|
||||||
# - framework: WidgetKit.framework
|
- framework: WidgetKit.framework
|
||||||
|
|
||||||
schemes:
|
schemes:
|
||||||
Kiwix:
|
Kiwix:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user