diff --git a/App/App_iOS.swift b/App/App_iOS.swift
index 8b1b628e..c862269f 100644
--- a/App/App_iOS.swift
+++ b/App/App_iOS.swift
@@ -25,14 +25,23 @@ struct Kiwix: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
private let fileMonitor: DirectoryMonitor
+ private let activityService: ActivityService?
init() {
fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) }
+ // MARK: - live activities
+ switch AppType.current {
+ case .kiwix:
+ activityService = nil //ActivityService()
+ case .custom:
+ activityService = nil
+ }
UNUserNotificationCenter.current().delegate = appDelegate
// MARK: - migrations
if !ProcessInfo.processInfo.arguments.contains("testing") {
_ = MigrationService().migrateAll()
}
+
}
var body: some Scene {
diff --git a/Common/DownloadActivityAttributes.swift b/Common/DownloadActivityAttributes.swift
new file mode 100644
index 00000000..79d9c40c
--- /dev/null
+++ b/Common/DownloadActivityAttributes.swift
@@ -0,0 +1,59 @@
+// This file is part of Kiwix for iOS & macOS.
+//
+// Kiwix is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 3 of the License, or
+// any later version.
+//
+// Kiwix is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Kiwix; If not, see https://www.gnu.org/licenses/.
+#if os(iOS)
+
+import ActivityKit
+
+public struct DownloadActivityAttributes: ActivityAttributes {
+
+ public let title: String
+
+ public init(title: String) {
+ self.title = title
+ }
+
+ public struct ContentState: Codable & Hashable {
+ public let items: [DownloadItem]
+ public var totalProgress: Double {
+ let sum = items.reduce(Double(0.0), { partialResult, item in
+ partialResult + item.progress
+ })
+ return sum / Double(items.count)
+ }
+
+ public init(items: [DownloadItem]) {
+ self.items = items
+ }
+ }
+
+ public struct DownloadItem: Codable & Hashable {
+ public let uuid: UUID
+ public let description: String
+ public let progress: Double
+
+ public init(uuid: UUID, description: String, progress: Double) {
+ self.uuid = uuid
+ self.description = description
+ self.progress = progress
+ }
+
+ public init(completedFor uuid: UUID) {
+ self.uuid = uuid
+ self.progress = 1.0
+ self.description = "Completed!" //TODO: update
+ }
+ }
+}
+#endif
diff --git a/Support/Info.plist b/Support/Info.plist
index 916b9d80..029fb8ad 100644
--- a/Support/Info.plist
+++ b/Support/Info.plist
@@ -63,5 +63,9 @@
+ NSSupportsLiveActivities
+
+ NSSupportsLiveActivitiesFrequentUpdates
+
diff --git a/Views/LiveActivity/ActivityService.swift b/Views/LiveActivity/ActivityService.swift
index 590dc3e6..09df3c4a 100644
--- a/Views/LiveActivity/ActivityService.swift
+++ b/Views/LiveActivity/ActivityService.swift
@@ -12,39 +12,39 @@
//
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
-//
-//#if os(iOS)
-//
+
+#if os(iOS)
+
import Combine
import ActivityKit
-import KiwixWidgetsExtension
-//
-//@MainActor
-//final class ActivityService {
-//
-// private var cancellables = Set()
-// private var activity: Activity?
-//
-// init(
-// publisher: @MainActor () -> CurrentValueSubject<[UUID: DownloadState], Never> = { DownloadService.shared.progress.publisher
-// }
-// ) {
-// publisher().sink { [weak self] (state: [UUID : DownloadState]) in
-// guard let self else { return }
-// if state.isEmpty {
-// stop()
-// } else {
-// update(state: state)
-// }
-// }.store(in: &cancellables)
-// }
-//
-// private func start(with state: [UUID: DownloadState]) {
-// let content = ActivityContent(
-// state: activityState(from: state),
-// staleDate: nil,
-// relevanceScore: 0.0
-// )
+
+@MainActor
+final class ActivityService {
+
+ private var cancellables = Set()
+ private var activity: Activity?
+
+ init(
+ publisher: @MainActor () -> CurrentValueSubject<[UUID: DownloadState], Never> = { DownloadService.shared.progress.publisher
+ }
+ ) {
+ publisher().sink { [weak self] (state: [UUID : DownloadState]) in
+ guard let self else { return }
+ if state.isEmpty {
+ stop()
+ } else {
+ update(state: state)
+ }
+ }.store(in: &cancellables)
+ }
+
+ private func start(with state: [UUID: DownloadState]) {
+ let content = ActivityContent(
+ state: activityState(from: state),
+ staleDate: nil,
+ relevanceScore: 0.0
+ )
+ debugPrint("start with: \(state)")
// if let activity = try? Activity
// .request(
// attributes: DownloadActivityAttributes(title: "Downloads"),
@@ -59,9 +59,10 @@ import KiwixWidgetsExtension
// }
// }
// }
-// }
-//
-// private func update(state: [UUID: DownloadState]) {
+ }
+
+ private func update(state: [UUID: DownloadState]) {
+ debugPrint("update state: \(state)")
// guard let activity else {
// start(with: state)
// return
@@ -74,9 +75,10 @@ import KiwixWidgetsExtension
// )
// )
// }
-// }
-//
-// private func stop() {
+ }
+
+ private func stop() {
+ debugPrint("stop")
// if let activity {
// let previousState = activity.content.state
// Task {
@@ -86,21 +88,21 @@ import KiwixWidgetsExtension
// self.activity = nil
// }
// }
-// }
-//
-// private func activityState(from state: [UUID: DownloadState]) -> DownloadActivityAttributes.ContentState {
-// DownloadActivityAttributes.ContentState(
-// items: state.map { (key: UUID, download: DownloadState)-> DownloadActivityAttributes.DownloadItem in
-// DownloadActivityAttributes.DownloadItem(uuid: key, description: key.uuidString, progress: Double(download.downloaded/download.total))
-// })
-// }
-//
-// private func completeState(for previousState: DownloadActivityAttributes.ContentState) -> DownloadActivityAttributes.ContentState {
-// DownloadActivityAttributes
-// .ContentState(items: previousState.items.map { item in
-// DownloadActivityAttributes.DownloadItem(completedFor: item.uuid)
-// })
-// }
-//}
-//
-//#endif
+ }
+
+ private func activityState(from state: [UUID: DownloadState]) -> DownloadActivityAttributes.ContentState {
+ DownloadActivityAttributes.ContentState(
+ items: state.map { (key: UUID, download: DownloadState)-> DownloadActivityAttributes.DownloadItem in
+ DownloadActivityAttributes.DownloadItem(uuid: key, description: key.uuidString, progress: Double(download.downloaded/download.total))
+ })
+ }
+
+ private func completeState(for previousState: DownloadActivityAttributes.ContentState) -> DownloadActivityAttributes.ContentState {
+ DownloadActivityAttributes
+ .ContentState(items: previousState.items.map { item in
+ DownloadActivityAttributes.DownloadItem(completedFor: item.uuid)
+ })
+ }
+}
+
+#endif
diff --git a/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json b/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000..eb878970
--- /dev/null
+++ b/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json b/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..23058801
--- /dev/null
+++ b/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Assets.xcassets/Contents.json b/Widgets/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/Widgets/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 00000000..eb878970
--- /dev/null
+++ b/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/DownloadsLiveActivity.swift b/Widgets/DownloadsLiveActivity.swift
new file mode 100644
index 00000000..5144fdb9
--- /dev/null
+++ b/Widgets/DownloadsLiveActivity.swift
@@ -0,0 +1,94 @@
+// This file is part of Kiwix for iOS & macOS.
+//
+// Kiwix is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 3 of the License, or
+// any later version.
+//
+// Kiwix is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Kiwix; If not, see https://www.gnu.org/licenses/.
+
+import ActivityKit
+import WidgetKit
+import SwiftUI
+
+struct DownloadsLiveActivity: Widget {
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: DownloadActivityAttributes.self) { context in
+ // Lock screen/banner UI goes here
+ VStack {
+ ForEach(context.state.items, id: \.uuid) { item in
+ HStack {
+ Text(item.description)
+ ProgressView(value: item.progress)
+ }
+
+ }
+ }
+ .activityBackgroundTint(Color.cyan)
+ .activitySystemActionForegroundColor(Color.black)
+
+ } dynamicIsland: { context in
+ DynamicIsland {
+ // Expanded UI goes here. Compose the expanded UI through
+ // various regions, like leading/trailing/center/bottom
+ DynamicIslandExpandedRegion(.leading) {
+ VStack {
+ ForEach(context.state.items, id: \.uuid) { item in
+ Text(item.description)
+ }
+ }
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ ProgressView(value: context.state.totalProgress)
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ ProgressView(value: context.state.totalProgress)
+ }
+ } compactLeading: {
+ ProgressView(value: context.state.totalProgress)
+ } compactTrailing: {
+ ProgressView(value: context.state.totalProgress)
+ } minimal: {
+ ProgressView(value: context.state.totalProgress)
+ }
+ .widgetURL(URL(string: "https://www.kiwix.org"))
+ .keylineTint(Color.red)
+ }
+ }
+}
+
+extension DownloadActivityAttributes {
+ fileprivate static var preview: DownloadActivityAttributes {
+ DownloadActivityAttributes(title: "Downloads")
+ }
+}
+
+extension DownloadActivityAttributes.ContentState {
+ fileprivate static var midProgress: DownloadActivityAttributes.ContentState {
+ DownloadActivityAttributes.ContentState(items: [
+ DownloadActivityAttributes.DownloadItem(uuid: UUID(), description: "First item", progress: 0.5),
+ DownloadActivityAttributes.DownloadItem(uuid: UUID(), description: "2nd item", progress: 0.9)
+ ])
+ }
+
+ fileprivate static var completed: DownloadActivityAttributes.ContentState {
+ DownloadActivityAttributes.ContentState(items: [
+ DownloadActivityAttributes.DownloadItem(uuid: UUID(), description: "First item", progress: 1.0),
+ DownloadActivityAttributes.DownloadItem(uuid: UUID(), description: "2nd item", progress: 0.8)
+ ])
+ }
+}
+
+@available(iOS 18.0, *)
+#Preview("Notification", as: .content, using: DownloadActivityAttributes.preview) {
+ DownloadsLiveActivity()
+} contentStates: {
+ DownloadActivityAttributes.ContentState.midProgress
+ DownloadActivityAttributes.ContentState.completed
+}
diff --git a/Widgets/Info.plist b/Widgets/Info.plist
new file mode 100644
index 00000000..0f118fb7
--- /dev/null
+++ b/Widgets/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/Widgets/WidgetsBundle.swift b/Widgets/WidgetsBundle.swift
new file mode 100644
index 00000000..251891f5
--- /dev/null
+++ b/Widgets/WidgetsBundle.swift
@@ -0,0 +1,25 @@
+// This file is part of Kiwix for iOS & macOS.
+//
+// Kiwix is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 3 of the License, or
+// any later version.
+//
+// Kiwix is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Kiwix; If not, see https://www.gnu.org/licenses/.
+
+import WidgetKit
+import SwiftUI
+
+@available(iOS 16.6, *)
+@main
+struct WidgetsBundle: WidgetBundle {
+ var body: some Widget {
+ DownloadsLiveActivity()
+ }
+}
diff --git a/project.yml b/project.yml
index 54650797..329babaa 100644
--- a/project.yml
+++ b/project.yml
@@ -2,7 +2,7 @@ name: Kiwix
options:
xcodeVersion: 15.2
deploymentTarget: # the three latest major versions should be supported
- iOS: 16.0
+ iOS: 16.6
macOS: 13.0
generateEmptyDirectories: true
useTabs: false
@@ -87,6 +87,9 @@ targetTemplates:
- path: PrivacyInfo.xcprivacy
destinationFilters:
- iOS
+ - path: Common
+ destinationFilters:
+ - iOS
- path: Contents
includes:
- Resources
@@ -136,6 +139,16 @@ targets:
- path: Tests
dependencies:
- target: Kiwix
+ Widgets:
+ type: app-extension
+ supportedDestinations: [iOS]
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.widgets
+ INFOPLIST_FILE: Widgets/Info.plist
+ sources:
+ - path: Common
+ - path: Widgets
schemes:
Kiwix: