kiwix-apple/Widgets/DownloadsLiveActivity.swift
Balazs Perlaki-Horvath 9f7afe6809 Use instant updates
2025-03-28 13:48:18 +01:00

207 lines
7.1 KiB
Swift

// 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 {
// @Environment(\.isActivityFullscreen) var isActivityFullScreen has a bug, when min iOS is 16
// https://developer.apple.com/forums/thread/763594
var body: some WidgetConfiguration {
ActivityConfiguration(for: DownloadActivityAttributes.self) { context in
// Lock screen/banner UI
VStack {
HStack {
VStack(alignment: .leading) {
titleFor(context.state.title)
progressFor(state: context.state)
}
.padding()
KiwixLogo(maxHeight: 50)
.padding(.trailing)
}
}
.modifier(WidgetBackgroundModifier())
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading) {
titleFor(context.state.title)
progressFor(state: context.state)
Spacer()
}
.padding()
.dynamicIsland(verticalPlacement: .belowIfTooWide)
}
DynamicIslandExpandedRegion(.trailing) {
KiwixLogo(maxHeight: 50)
.padding()
}
} compactLeading: {
KiwixLogo()
} compactTrailing: {
ProgressView(value: context.state.progress)
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
.frame(width: 20, height: 20, alignment: .trailing)
} minimal: {
ProgressView(value: context.state.progress)
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
.frame(width: 24, height: 24)
}
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
.keylineTint(Color.red)
}.containerBackgroundRemovable()
}
@ViewBuilder
private func titleFor(_ title: String) -> some View {
Text(title)
.lineLimit(1)
.frame(minWidth: 150, alignment: .leading)
.font(.headline)
.bold()
}
@ViewBuilder
private func progressText(_ description: String) -> some View {
Text(description)
.lineLimit(1)
.font(.caption)
.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
) -> some View {
let timeInterval = currentTimeInterval(state: state)
ProgressView(value: state.progress, label: {
progressText(state.progressDescription)
}, currentValueLabel: {
if !state.isAllPaused {
Text(timerInterval: timeInterval)
.font(.caption)
.tint(.secondary)
} else {
Label("", systemImage: "pause.fill")
.font(.caption)
.tint(.secondary)
}
})
.tint(Color.primary)
}
}
extension DownloadActivityAttributes {
fileprivate static var preview: DownloadActivityAttributes {
DownloadActivityAttributes()
}
}
extension DownloadActivityAttributes.ContentState {
fileprivate static var midProgress: DownloadActivityAttributes.ContentState {
DownloadActivityAttributes.ContentState(
downloadingTitle: "Downloading...",
items: [
DownloadActivityAttributes.DownloadItem(
uuid: UUID(),
description: "First item",
downloaded: 128,
total: 256,
timeRemaining: 15,
isPaused: true
)
]
)
}
fileprivate static var completed: DownloadActivityAttributes.ContentState {
DownloadActivityAttributes.ContentState(
downloadingTitle: "Downloading...",
items: [
DownloadActivityAttributes.DownloadItem(
uuid: UUID(),
description: "First item",
downloaded: 256,
total: 256,
timeRemaining: 0,
isPaused: false
),
DownloadActivityAttributes.DownloadItem(
uuid: UUID(),
description: "2nd item",
downloaded: 110,
total: 124,
timeRemaining: 20,
isPaused: false
)
]
)
}
}
@available(iOS 18.0, *)
#Preview("Notification", as: .content, using: DownloadActivityAttributes.preview) {
DownloadsLiveActivity()
} contentStates: {
DownloadActivityAttributes.ContentState.midProgress
DownloadActivityAttributes.ContentState.completed
}
@available(iOS 18.0, *)
#Preview("Compact", as: .dynamicIsland(.compact), using: DownloadActivityAttributes.preview) {
DownloadsLiveActivity()
} contentStates: {
DownloadActivityAttributes.ContentState.midProgress
DownloadActivityAttributes.ContentState.completed
}
@available(iOS 18.0, *)
#Preview("Minimal", as: .dynamicIsland(.minimal), using: DownloadActivityAttributes.preview) {
DownloadsLiveActivity()
} contentStates: {
DownloadActivityAttributes.ContentState.midProgress
DownloadActivityAttributes.ContentState.completed
}
@available(iOS 18.0, *)
#Preview("Dynamic island", as: .dynamicIsland(.expanded), using: DownloadActivityAttributes.preview) {
DownloadsLiveActivity()
} contentStates: {
DownloadActivityAttributes.ContentState.midProgress
DownloadActivityAttributes.ContentState.completed
}