Generate widgets

This commit is contained in:
Balazs Perlaki-Horvath 2025-01-30 21:59:07 +01:00 committed by Kelson
parent ca833c31a4
commit fd3e5daa3f
12 changed files with 336 additions and 56 deletions

View File

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

View File

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

View File

@ -63,5 +63,9 @@
</dict>
</dict>
</array>
<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>
</dict>
</plist>

View File

@ -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<AnyCancellable>()
// private var activity: Activity<DownloadActivityAttributes>?
//
// 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<AnyCancellable>()
private var activity: Activity<DownloadActivityAttributes>?
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

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

11
Widgets/Info.plist Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

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

View File

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