mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-24 12:13:32 -04:00
Merge pull request #1126 from kiwix/live-activities-update
Use progress bar with timer updates for live activities
This commit is contained in:
commit
83518f2640
@ -52,7 +52,15 @@ public struct DownloadActivityAttributes: ActivityAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var estimatedTimeLeft: TimeInterval {
|
public var estimatedTimeLeft: TimeInterval {
|
||||||
items.map(\.timeRemaining).max() ?? 0
|
items.filter { (item: DownloadActivityAttributes.DownloadItem) in
|
||||||
|
!item.isPaused
|
||||||
|
}.map(\.timeRemaining).max() ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAllPaused: Bool {
|
||||||
|
items.allSatisfy { (item: DownloadActivityAttributes.DownloadItem) in
|
||||||
|
item.isPaused
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var progress: Double {
|
public var progress: Double {
|
||||||
@ -70,6 +78,7 @@ public struct DownloadActivityAttributes: ActivityAttributes {
|
|||||||
let downloaded: Int64
|
let downloaded: Int64
|
||||||
let total: Int64
|
let total: Int64
|
||||||
let timeRemaining: TimeInterval
|
let timeRemaining: TimeInterval
|
||||||
|
let isPaused: Bool
|
||||||
var progress: Double {
|
var progress: Double {
|
||||||
progressFor(items: [self]).fractionCompleted
|
progressFor(items: [self]).fractionCompleted
|
||||||
}
|
}
|
||||||
@ -77,12 +86,20 @@ public struct DownloadActivityAttributes: ActivityAttributes {
|
|||||||
progressFor(items: [self]).localizedAdditionalDescription
|
progressFor(items: [self]).localizedAdditionalDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(uuid: UUID, description: String, downloaded: Int64, total: Int64, timeRemaining: TimeInterval) {
|
public init(
|
||||||
|
uuid: UUID,
|
||||||
|
description: String,
|
||||||
|
downloaded: Int64,
|
||||||
|
total: Int64,
|
||||||
|
timeRemaining: TimeInterval,
|
||||||
|
isPaused: Bool
|
||||||
|
) {
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.description = description
|
self.description = description
|
||||||
self.downloaded = downloaded
|
self.downloaded = downloaded
|
||||||
self.total = total
|
self.total = total
|
||||||
self.timeRemaining = timeRemaining
|
self.timeRemaining = timeRemaining
|
||||||
|
self.isPaused = isPaused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,102 +13,11 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||||
|
|
||||||
//
|
|
||||||
// DownloadService.swift
|
|
||||||
// Kiwix
|
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import os
|
import os
|
||||||
|
|
||||||
struct DownloadState: Codable {
|
|
||||||
let downloaded: Int64
|
|
||||||
let total: Int64
|
|
||||||
let resumeData: Data?
|
|
||||||
|
|
||||||
static func empty() -> DownloadState {
|
|
||||||
.init(downloaded: 0, total: 1, resumeData: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(downloaded: Int64, total: Int64, resumeData: Data?) {
|
|
||||||
guard total >= downloaded, total > 0 else {
|
|
||||||
assertionFailure("invalid download progress values: downloaded \(downloaded) total: \(total)")
|
|
||||||
self.downloaded = downloaded
|
|
||||||
self.total = downloaded
|
|
||||||
self.resumeData = resumeData
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.downloaded = downloaded
|
|
||||||
self.total = total
|
|
||||||
self.resumeData = resumeData
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatedWith(downloaded: Int64, total: Int64) -> DownloadState {
|
|
||||||
DownloadState(downloaded: downloaded, total: total, resumeData: resumeData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatedWith(resumeData: Data?) -> DownloadState {
|
|
||||||
DownloadState(downloaded: downloaded, total: total, resumeData: resumeData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
final class DownloadTasksPublisher {
|
|
||||||
|
|
||||||
let publisher: CurrentValueSubject<[UUID: DownloadState], Never>
|
|
||||||
private var states = [UUID: DownloadState]()
|
|
||||||
|
|
||||||
init() {
|
|
||||||
publisher = CurrentValueSubject(states)
|
|
||||||
if let jsonData = UserDefaults.standard.object(forKey: "downloadStates") as? Data,
|
|
||||||
let storedStates = try? JSONDecoder().decode([UUID: DownloadState].self, from: jsonData) {
|
|
||||||
states = storedStates
|
|
||||||
publisher.send(states)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateFor(uuid: UUID, downloaded: Int64, total: Int64) {
|
|
||||||
if let state = states[uuid] {
|
|
||||||
states[uuid] = state.updatedWith(downloaded: downloaded, total: total)
|
|
||||||
} else {
|
|
||||||
states[uuid] = DownloadState(downloaded: downloaded, total: total, resumeData: nil)
|
|
||||||
}
|
|
||||||
publisher.send(states)
|
|
||||||
saveState()
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetFor(uuid: UUID) {
|
|
||||||
states.removeValue(forKey: uuid)
|
|
||||||
publisher.send(states)
|
|
||||||
saveState()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmpty() -> Bool {
|
|
||||||
states.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
func resumeDataFor(uuid: UUID) -> Data? {
|
|
||||||
states[uuid]?.resumeData
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateFor(uuid: UUID, withResumeData resumeData: Data?) {
|
|
||||||
if let state = states[uuid] {
|
|
||||||
states[uuid] = state.updatedWith(resumeData: resumeData)
|
|
||||||
publisher.send(states)
|
|
||||||
saveState()
|
|
||||||
} else {
|
|
||||||
assertionFailure("there should be a download task for: \(uuid)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveState() {
|
|
||||||
if let jsonStates = try? JSONEncoder().encode(states) {
|
|
||||||
UserDefaults.standard.setValue(jsonStates, forKey: "downloadStates")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
|
final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
|
||||||
static let shared = DownloadService()
|
static let shared = DownloadService()
|
||||||
private let queue = DispatchQueue(label: "downloads", qos: .background)
|
private let queue = DispatchQueue(label: "downloads", qos: .background)
|
52
Model/Downloads/DownloadState.swift
Normal file
52
Model/Downloads/DownloadState.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// 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 Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
struct DownloadState: Codable {
|
||||||
|
let downloaded: Int64
|
||||||
|
let total: Int64
|
||||||
|
let resumeData: Data?
|
||||||
|
|
||||||
|
var isPaused: Bool {
|
||||||
|
resumeData != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func empty() -> DownloadState {
|
||||||
|
.init(downloaded: 0, total: 1, resumeData: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(downloaded: Int64, total: Int64, resumeData: Data?) {
|
||||||
|
guard total >= downloaded, total > 0 else {
|
||||||
|
assertionFailure("invalid download progress values: downloaded \(downloaded) total: \(total)")
|
||||||
|
self.downloaded = downloaded
|
||||||
|
self.total = downloaded
|
||||||
|
self.resumeData = resumeData
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.downloaded = downloaded
|
||||||
|
self.total = total
|
||||||
|
self.resumeData = resumeData
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatedWith(downloaded: Int64, total: Int64) -> DownloadState {
|
||||||
|
DownloadState(downloaded: downloaded, total: total, resumeData: resumeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatedWith(resumeData: Data?) -> DownloadState {
|
||||||
|
DownloadState(downloaded: downloaded, total: total, resumeData: resumeData)
|
||||||
|
}
|
||||||
|
}
|
73
Model/Downloads/DownloadTasksPublisher.swift
Normal file
73
Model/Downloads/DownloadTasksPublisher.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// 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 Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class DownloadTasksPublisher {
|
||||||
|
|
||||||
|
let publisher: CurrentValueSubject<[UUID: DownloadState], Never>
|
||||||
|
private var states = [UUID: DownloadState]()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
publisher = CurrentValueSubject(states)
|
||||||
|
if let jsonData = UserDefaults.standard.object(forKey: "downloadStates") as? Data,
|
||||||
|
let storedStates = try? JSONDecoder().decode([UUID: DownloadState].self, from: jsonData) {
|
||||||
|
states = storedStates
|
||||||
|
publisher.send(states)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFor(uuid: UUID, downloaded: Int64, total: Int64) {
|
||||||
|
if let state = states[uuid] {
|
||||||
|
states[uuid] = state.updatedWith(downloaded: downloaded, total: total)
|
||||||
|
} else {
|
||||||
|
states[uuid] = DownloadState(downloaded: downloaded, total: total, resumeData: nil)
|
||||||
|
}
|
||||||
|
publisher.send(states)
|
||||||
|
saveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetFor(uuid: UUID) {
|
||||||
|
states.removeValue(forKey: uuid)
|
||||||
|
publisher.send(states)
|
||||||
|
saveState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmpty() -> Bool {
|
||||||
|
states.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumeDataFor(uuid: UUID) -> Data? {
|
||||||
|
states[uuid]?.resumeData
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFor(uuid: UUID, withResumeData resumeData: Data?) {
|
||||||
|
if let state = states[uuid] {
|
||||||
|
states[uuid] = state.updatedWith(resumeData: resumeData)
|
||||||
|
publisher.send(states)
|
||||||
|
saveState()
|
||||||
|
} else {
|
||||||
|
assertionFailure("there should be a download task for: \(uuid)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveState() {
|
||||||
|
if let jsonStates = try? JSONEncoder().encode(states) {
|
||||||
|
UserDefaults.standard.setValue(jsonStates, forKey: "downloadStates")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -611,8 +611,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
|
|||||||
)
|
)
|
||||||
actions.append(
|
actions.append(
|
||||||
UIAction(title: LocalString.common_dialog_button_open_in_new_tab,
|
UIAction(title: LocalString.common_dialog_button_open_in_new_tab,
|
||||||
image: UIImage(systemName: "doc.badge.plus")) { [weak self] _ in
|
image: UIImage(systemName: "doc.badge.plus")) { _ in
|
||||||
guard let self else { return }
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
NotificationCenter.openURL(url, inNewTab: true)
|
NotificationCenter.openURL(url, inNewTab: true)
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
// General Public License for more details.
|
// General Public License for more details.
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Kiwix; If not, see https://www.gnu.orgllll/llicenses/.
|
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
||||||
@ -49,9 +49,9 @@ final class ActivityService {
|
|||||||
publisher().sink { [weak self] (state: [UUID: DownloadState]) in
|
publisher().sink { [weak self] (state: [UUID: DownloadState]) in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
if state.isEmpty {
|
if state.isEmpty {
|
||||||
stop()
|
self.stop()
|
||||||
} else {
|
} else {
|
||||||
update(state: state)
|
self.update(state: state)
|
||||||
}
|
}
|
||||||
}.store(in: &cancellables)
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
@ -93,19 +93,31 @@ final class ActivityService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let now = CACurrentMediaTime()
|
let now = CACurrentMediaTime()
|
||||||
guard let activity, (now - lastUpdate) > updateFrequency else {
|
// make sure we don't update too frequently
|
||||||
|
// unless there's a pause, we do want immediate update
|
||||||
|
let isTooEarlyToUpdate = if hasAnyPause(in: state) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
(now - lastUpdate) <= updateFrequency
|
||||||
|
}
|
||||||
|
guard let activity, !isTooEarlyToUpdate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastUpdate = now
|
|
||||||
Task {
|
Task {
|
||||||
let activityState = await activityState(from: state, downloadTimes: downloadTimes)
|
let activityState = await activityState(from: state, downloadTimes: downloadTimes)
|
||||||
await activity.update(
|
let newContent = ActivityContent<DownloadActivityAttributes.ContentState>(
|
||||||
ActivityContent<DownloadActivityAttributes.ContentState>(
|
state: activityState,
|
||||||
state: activityState,
|
staleDate: nil
|
||||||
staleDate: nil
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if #available(iOS 17.2, *) {
|
||||||
|
// important to define a timestamp, this way iOS knows which updates
|
||||||
|
// can be dropped, if too many of them queues up
|
||||||
|
await activity.update(newContent, timestamp: Date.now)
|
||||||
|
} else {
|
||||||
|
await activity.update(newContent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
lastUpdate = now
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatedDownloadTimes(from states: [UUID: DownloadState]) -> [UUID: CFTimeInterval] {
|
private func updatedDownloadTimes(from states: [UUID: DownloadState]) -> [UUID: CFTimeInterval] {
|
||||||
@ -171,9 +183,18 @@ final class ActivityService {
|
|||||||
description: titles[key] ?? key.uuidString,
|
description: titles[key] ?? key.uuidString,
|
||||||
downloaded: download.downloaded,
|
downloaded: download.downloaded,
|
||||||
total: download.total,
|
total: download.total,
|
||||||
timeRemaining: downloadTimes[key] ?? 0)
|
timeRemaining: downloadTimes[key] ?? 0,
|
||||||
|
isPaused: download.isPaused
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func hasAnyPause(in state: [UUID: DownloadState]) -> Bool {
|
||||||
|
guard !state.isEmpty else { return false }
|
||||||
|
return !state.values.allSatisfy { (download: DownloadState) in
|
||||||
|
download.isPaused == false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -21,73 +21,51 @@ 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 goes here
|
// Lock screen/banner UI
|
||||||
|
let timeInterval = startTime...Date(
|
||||||
|
timeInterval: context.state.estimatedTimeLeft,
|
||||||
|
since: .now
|
||||||
|
)
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
KiwixLogo(maxHeight: 50)
|
|
||||||
.padding()
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(context.state.title)
|
titleFor(context.state.title)
|
||||||
.lineLimit(1)
|
progressFor(state: context.state, timeInterval: timeInterval)
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.font(.headline)
|
|
||||||
.bold()
|
|
||||||
HStack {
|
|
||||||
Text(
|
|
||||||
timerInterval: Date.now...Date(
|
|
||||||
timeInterval: context.state.estimatedTimeLeft,
|
|
||||||
since: .now
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.lineLimit(1)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.font(.caption)
|
|
||||||
.tint(.secondary)
|
|
||||||
Text(context.state.progressDescription)
|
|
||||||
.lineLimit(1)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.font(.caption)
|
|
||||||
.tint(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Spacer()
|
.padding()
|
||||||
ProgressView(value: context.state.progress)
|
KiwixLogo(maxHeight: 50)
|
||||||
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
|
.padding(.trailing)
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(WidgetBackgroundModifier())
|
.modifier(WidgetBackgroundModifier())
|
||||||
|
|
||||||
} dynamicIsland: { context in
|
} dynamicIsland: { context in
|
||||||
DynamicIsland {
|
DynamicIsland {
|
||||||
// Expanded UI goes here. Compose the expanded UI through
|
// Expanded UI
|
||||||
// various regions, like leading/trailing/center/bottom
|
|
||||||
DynamicIslandExpandedRegion(.leading) {
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
Spacer()
|
let timeInterval = startTime...Date(
|
||||||
KiwixLogo(maxHeight: 50)
|
timeInterval: context.state.estimatedTimeLeft,
|
||||||
Spacer()
|
since: .now
|
||||||
}
|
)
|
||||||
DynamicIslandExpandedRegion(.trailing) {
|
|
||||||
ProgressView(value: context.state.progress)
|
|
||||||
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 11.4))
|
|
||||||
.padding(6.0)
|
|
||||||
}
|
|
||||||
DynamicIslandExpandedRegion(.center) {
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(context.state.title)
|
titleFor(context.state.title)
|
||||||
.lineLimit(1)
|
progressFor(state: context.state, timeInterval: timeInterval)
|
||||||
.multilineTextAlignment(.leading)
|
Spacer()
|
||||||
.font(.headline)
|
|
||||||
.bold()
|
|
||||||
Text(context.state.progressDescription)
|
|
||||||
.lineLimit(1)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.font(.caption)
|
|
||||||
.tint(.secondary)
|
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
|
.dynamicIsland(verticalPlacement: .belowIfTooWide)
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicIslandExpandedRegion(.trailing) {
|
||||||
|
KiwixLogo(maxHeight: 50)
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
} compactLeading: {
|
} compactLeading: {
|
||||||
KiwixLogo()
|
KiwixLogo()
|
||||||
@ -105,6 +83,49 @@ struct DownloadsLiveActivity: Widget {
|
|||||||
.keylineTint(Color.red)
|
.keylineTint(Color.red)
|
||||||
}.containerBackgroundRemovable()
|
}.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func progressFor(
|
||||||
|
state: DownloadActivityAttributes.ContentState,
|
||||||
|
timeInterval: ClosedRange<Date>
|
||||||
|
) -> some View {
|
||||||
|
if !state.isAllPaused {
|
||||||
|
ProgressView(timerInterval: timeInterval, countsDown: false, label: {
|
||||||
|
progressText(state.progressDescription)
|
||||||
|
}, currentValueLabel: {
|
||||||
|
Text(timerInterval: timeInterval)
|
||||||
|
.font(.caption)
|
||||||
|
.tint(.secondary)
|
||||||
|
})
|
||||||
|
.tint(Color.primary)
|
||||||
|
} else {
|
||||||
|
ProgressView(value: state.progress, label: {
|
||||||
|
progressText(state.progressDescription)
|
||||||
|
}, currentValueLabel: {
|
||||||
|
Label("", systemImage: "pause.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.tint(.secondary)
|
||||||
|
})
|
||||||
|
.tint(Color.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DownloadActivityAttributes {
|
extension DownloadActivityAttributes {
|
||||||
@ -123,14 +144,16 @@ extension DownloadActivityAttributes.ContentState {
|
|||||||
description: "First item",
|
description: "First item",
|
||||||
downloaded: 128,
|
downloaded: 128,
|
||||||
total: 256,
|
total: 256,
|
||||||
timeRemaining: 3
|
timeRemaining: 15,
|
||||||
|
isPaused: true
|
||||||
),
|
),
|
||||||
DownloadActivityAttributes.DownloadItem(
|
DownloadActivityAttributes.DownloadItem(
|
||||||
uuid: UUID(),
|
uuid: UUID(),
|
||||||
description: "2nd item",
|
description: "2nd item",
|
||||||
downloaded: 90,
|
downloaded: 90,
|
||||||
total: 124,
|
total: 124,
|
||||||
timeRemaining: 2
|
timeRemaining: 2,
|
||||||
|
isPaused: true
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -145,14 +168,16 @@ extension DownloadActivityAttributes.ContentState {
|
|||||||
description: "First item",
|
description: "First item",
|
||||||
downloaded: 256,
|
downloaded: 256,
|
||||||
total: 256,
|
total: 256,
|
||||||
timeRemaining: 0
|
timeRemaining: 0,
|
||||||
|
isPaused: false
|
||||||
),
|
),
|
||||||
DownloadActivityAttributes.DownloadItem(
|
DownloadActivityAttributes.DownloadItem(
|
||||||
uuid: UUID(),
|
uuid: UUID(),
|
||||||
description: "2nd item",
|
description: "2nd item",
|
||||||
downloaded: 110,
|
downloaded: 110,
|
||||||
total: 124,
|
total: 124,
|
||||||
timeRemaining: 2
|
timeRemaining: 2,
|
||||||
|
isPaused: false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -31,7 +31,7 @@ struct KiwixLogo: View {
|
|||||||
Image("KiwixLogo")
|
Image("KiwixLogo")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: maxHeight / 1.6182, height: maxHeight / 1.6182)
|
.frame(width: maxHeight * 0.75, height: maxHeight * 0.75)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user