Live activities and various improvements

main
Laurent 3 years ago
parent c0731189ad
commit c1bdac59ff
  1. 33
      LaunchWidget/LaunchWidgetLiveActivity.swift
  2. 60
      LeCountdown/Conductor.swift
  3. 1
      LeCountdown/Model/LiveTimer.swift
  4. 26
      LeCountdown/Sound/DelaySoundPlayer.swift
  5. 1
      LeCountdown/Subscription/SubscriptionButtonView.swift
  6. 2
      LeCountdown/Views/Countdown/NewCountdownView.swift
  7. 114
      LeCountdown/Views/LiveTimerListView.swift
  8. 1
      LeCountdown/Widget/LaunchWidgetAttributes.swift
  9. 2
      LeCountdown/fr.lproj/Localizable.strings

@ -32,33 +32,26 @@ struct LaunchWidgetLiveActivity: Widget {
var body: some WidgetConfiguration { var body: some WidgetConfiguration {
ActivityConfiguration(for: LaunchWidgetAttributes.self) { context in ActivityConfiguration(for: LaunchWidgetAttributes.self) { context in
// let range = Date()...context.attributes.date
// Lock screen/banner UI goes here // Lock screen/banner UI goes here
HStack { HStack {
Text(context.attributes.name.uppercased()) Text(context.attributes.name.uppercased())
Spacer() Spacer()
Text(context.attributes.date, style: .timer)
.font(.title) if context.attributes.isTimer {
let range = Date()...context.attributes.date
// if Date() < context.attributes.date { Text(timerInterval: range,
// Text(context.attributes.date, style: .timer) pauseTime: range.lowerBound)
// } else { .font(.title)
// GreenCheckmarkView() } else {
// } Text(context.attributes.date, style: .timer)
.font(.title)
// if context.attributes.endDate > self.model.now { }
// Text(context.attributes.endDate, style: .timer)
// .monospaced()
// } else {
// Text("It's time!")
// }
}.padding() }.padding()
.monospaced() .monospaced()
.foregroundColor(.white) .foregroundColor(.white)
.activityBackgroundTint(Color(white: 0.2)) .activityBackgroundTint(Color(red: 1.0, green: 0.5, blue: 0.4))
.activitySystemActionForegroundColor(.white) .activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in } dynamicIsland: { context in
DynamicIsland { DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through // Expanded UI goes here. Compose the expanded UI through
@ -100,7 +93,7 @@ struct LaunchWidgetLiveActivity_Previews: PreviewProvider {
static let attributes = LaunchWidgetAttributes( static let attributes = LaunchWidgetAttributes(
id: "", id: "",
name: "Tea", name: "Tea",
date: Date().addingTimeInterval(3600.0)) date: Date().addingTimeInterval(3600.0), isTimer: true)
static let contentState = LaunchWidgetAttributes.ContentState(ended: false) static let contentState = LaunchWidgetAttributes.ContentState(ended: false)

@ -64,6 +64,7 @@ class Conductor: ObservableObject {
func removeLiveTimer(id: TimerID) { func removeLiveTimer(id: TimerID) {
// Logger.log("removeLiveTimer") // Logger.log("removeLiveTimer")
self.liveTimers.removeAll(where: { $0.id == id }) self.liveTimers.removeAll(where: { $0.id == id })
self.currentStopwatches.removeValue(forKey: id)
self.cancelledCountdowns.removeAll(where: { $0 == id }) self.cancelledCountdowns.removeAll(where: { $0 == id })
if let soundPlayer = self._delayedSoundPlayers[id] { if let soundPlayer = self._delayedSoundPlayers[id] {
soundPlayer.stop() soundPlayer.stop()
@ -93,7 +94,7 @@ class Conductor: ObservableObject {
} }
let liveStopwatches: [LiveTimer] = self.currentStopwatches.map { let liveStopwatches: [LiveTimer] = self.currentStopwatches.map {
return LiveTimer(id: $0, date: $1.start) return LiveTimer(id: $0, date: $1.start, endDate: $1.end)
} }
for liveStopwatch in liveStopwatches { for liveStopwatch in liveStopwatches {
if let index = self.liveTimers.firstIndex(where: { $0.id == liveStopwatch.id }) { if let index = self.liveTimers.firstIndex(where: { $0.id == liveStopwatch.id }) {
@ -167,6 +168,7 @@ class Conductor: ObservableObject {
if Preferences.playConfirmationSound { if Preferences.playConfirmationSound {
self._playConfirmationSound(timer: countdown) self._playConfirmationSound(timer: countdown)
} }
self._launchLiveActivity(timer: countdown, date: date)
handler(.success(date)) handler(.success(date))
} catch { } catch {
@ -181,13 +183,19 @@ class Conductor: ObservableObject {
func startStopwatch(_ stopwatch: Stopwatch) { func startStopwatch(_ stopwatch: Stopwatch) {
DispatchQueue.main.async { DispatchQueue.main.async {
let lsw = LiveStopWatch(start: Date()) let lsw = LiveStopWatch(start: Date())
Conductor.maestro.currentStopwatches[stopwatch.stringId] = lsw
// if let liveTimer = liveTimers.first(where: { $0.id == stopwatch.stringId }) {
// liveTimer.
// }
self.currentStopwatches[stopwatch.stringId] = lsw
if Preferences.playConfirmationSound { if Preferences.playConfirmationSound {
self._playSound(Const.confirmationSound.rawValue) self._playSound(Const.confirmationSound.rawValue)
} }
self._launchLiveActivity(stopwatch: stopwatch, start: lsw.start) self._endLiveActivity(timerId: stopwatch.stringId)
self._launchLiveActivity(timer: stopwatch, date: lsw.start)
// self._createTimerIntent(stopwatch) // self._createTimerIntent(stopwatch)
@ -200,7 +208,9 @@ class Conductor: ObservableObject {
if lsw.end == nil { if lsw.end == nil {
let end = Date() let end = Date()
lsw.end = end lsw.end = end
// Conductor.maestro.currentStopwatches.removeValue(forKey: stopwatch.stringId)
Conductor.maestro.currentStopwatches[stopwatch.stringId] = lsw
do { do {
try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: lsw.start, end: end)) try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: lsw.start, end: end))
} catch { } catch {
@ -238,6 +248,7 @@ class Conductor: ObservableObject {
func cleanup() { func cleanup() {
self._cleanupCountdowns() self._cleanupCountdowns()
self.cleanupLiveActivities()
withAnimation { withAnimation {
self._cleanupLiveTimers() self._cleanupLiveTimers()
@ -368,19 +379,19 @@ class Conductor: ObservableObject {
// MARK: - Live Activity // MARK: - Live Activity
fileprivate func _launchLiveActivity(stopwatch: Stopwatch, start: Date) { fileprivate func _launchLiveActivity(timer: AbstractTimer, date: Date) {
if #available(iOS 16.2, *) { if #available(iOS 16.2, *) {
if ActivityAuthorizationInfo().areActivitiesEnabled { if ActivityAuthorizationInfo().areActivitiesEnabled {
let contentState = LaunchWidgetAttributes.ContentState(ended: false) let contentState = LaunchWidgetAttributes.ContentState(ended: false)
let attributes = LaunchWidgetAttributes(id: stopwatch.stringId, name: stopwatch.displayName, date: start) let attributes = LaunchWidgetAttributes(id: timer.stringId, name: timer.displayName, date: date, isTimer: timer is Countdown)
let activityContent = ActivityContent(state: contentState, staleDate: nil) let activityContent = ActivityContent(state: contentState, staleDate: nil)
do { do {
let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent)
print("Requested a Live Activity: \(String(describing: liveActivity.id)).") print("Requested a Live Activity: \(String(describing: liveActivity.id))")
} catch (let error) { } catch (let error) {
Logger.error(error) Logger.error(error)
} }
@ -392,41 +403,36 @@ class Conductor: ObservableObject {
} }
fileprivate func _liveActivity(timerId: String) -> ActivityKit.Activity<LaunchWidgetAttributes>? { fileprivate func _liveActivity(timerId: String) -> [ActivityKit.Activity<LaunchWidgetAttributes>] {
return ActivityKit.Activity<LaunchWidgetAttributes>.activities.first(where: { $0.attributes.id == timerId } ) return ActivityKit.Activity<LaunchWidgetAttributes>.activities.filter { $0.id == timerId }
}
fileprivate func _liveActivityIds() -> [String] {
return ActivityKit.Activity<LaunchWidgetAttributes>.activities.map { $0.id }
}
func cleanupLiveActivities() {
for id in self._liveActivityIds() {
if self.liveTimers.first(where: { $0.id == id} ) == nil {
self._endLiveActivity(timerId: id)
}
}
} }
func updateLiveActivities() { func updateLiveActivities() {
print("update live activity...") print("update live activity...")
for (countdownId, interval) in self.currentCountdowns { for (countdownId, interval) in self.currentCountdowns {
if interval.end < Date() { if interval.end < Date() {
self._endLiveActivity(timerId: countdownId) self._endLiveActivity(timerId: countdownId)
} }
// if let activity = self._liveActivity(countdownId: countdownId) {
//
// Task {
//
// if ended {
// self._endLiveActivity(countdownId: countdownId)
// }
//
//// let state = LaunchWidgetAttributes.ContentState(ended: ended)
//// let content = ActivityContent(state: state, staleDate: interval.end)
//// await activity.update(content)
//// print("Ending the Live Activity: \(activity.id)")
// }
// }
} }
} }
fileprivate func _endLiveActivity(timerId: String) { fileprivate func _endLiveActivity(timerId: String) {
if #available(iOS 16.2, *) { if #available(iOS 16.2, *) {
print("Try to end the Live Activity: \(timerId)") print("Try to end the Live Activity: \(timerId)")
if let activity = self._liveActivity(timerId: timerId) { for activity in self._liveActivity(timerId: timerId) {
Task { Task {
let state = LaunchWidgetAttributes.ContentState(ended: true) let state = LaunchWidgetAttributes.ContentState(ended: true)
let content = ActivityContent(state: state, staleDate: Date()) let content = ActivityContent(state: state, staleDate: Date())

@ -11,6 +11,7 @@ import CoreData
struct LiveTimer: Identifiable, Comparable { struct LiveTimer: Identifiable, Comparable {
var id: String var id: String
var date: Date var date: Date
var endDate: Date?
static func < (lhs: LiveTimer, rhs: LiveTimer) -> Bool { static func < (lhs: LiveTimer, rhs: LiveTimer) -> Bool {
return lhs.date < rhs.date return lhs.date < rhs.date

@ -42,10 +42,6 @@ import AVFoundation
fileprivate func _play(in duration: TimeInterval, repeatCount: Int) throws { fileprivate func _play(in duration: TimeInterval, repeatCount: Int) throws {
// let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
// try audioSession.setCategory(.playback, options: .duckOthers)
// try audioSession.setActive(true)
self._player.prepareToPlay() self._player.prepareToPlay()
self._player.volume = 1.0 self._player.volume = 1.0
self._player.delegate = self self._player.delegate = self
@ -54,38 +50,18 @@ import AVFoundation
Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)") Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)")
self._player.play(atTime: self._player.deviceCurrentTime + duration) self._player.play(atTime: self._player.deviceCurrentTime + duration)
// if repeatCount == 0 {
// self._scheduleFadeOut(duration: duration)
// }
} }
func stop() { func stop() {
self._player.stop() self._player.stop()
} }
// fileprivate func _scheduleFadeOut(duration: TimeInterval) {
// Logger.log("_scheduleFadeOut")
// guard let soundDuration = self._soundDuration, soundDuration > 1.0 else {
// return
// }
//
// let interval = duration + soundDuration - 1.0
// Logger.log("Fade in \(interval)")
// let date = Date(timeIntervalSinceNow: interval)
// Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
// Logger.log("FADEOUT!")
// self._player.setVolume(0.0, fadeDuration: 1.0)
// }
// self._timer?.fire()
// }
// MARK: - Delegate // MARK: - Delegate
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
Logger.log("audioPlayerDidFinishPlaying: successfully = \(flag)") Logger.log("audioPlayerDidFinishPlaying: successfully = \(flag)")
Conductor.maestro.cancelSoundPlayer(id: self._timerID) Conductor.maestro.cancelSoundPlayer(id: self._timerID)
// self._player.volume = 1.0 Conductor.maestro.cleanupLiveActivities()
} }
} }

@ -29,6 +29,7 @@ struct SubscriptionButtonView: View {
.buttonStyle(.bordered) .buttonStyle(.bordered)
.foregroundColor(.white) .foregroundColor(.white)
} }
} }
struct SubscriptionButtonView_Previews: PreviewProvider { struct SubscriptionButtonView_Previews: PreviewProvider {

@ -254,7 +254,7 @@ struct CountdownEditView : View {
fileprivate func _loadCountdown(_ countdown: Countdown) { fileprivate func _loadCountdown(_ countdown: Countdown) {
let hours = Int(countdown.duration / 3600.0) let hours = Int(countdown.duration / 3600.0)
let minutes = countdown.duration - Double(hours * 3600) let minutes = Int(countdown.duration - Double(hours * 3600)) / 60
let seconds = countdown.duration - Double(hours * 3600) - Double(minutes * 60) let seconds = countdown.duration - Double(hours * 3600) - Double(minutes * 60)
if hours > 0 { if hours > 0 {

@ -23,27 +23,79 @@ struct TimeView: View {
} }
struct LiveStopwatchView: View { struct LiveTimerListView: View {
@Environment(\.managedObjectContext) private var viewContext @Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var conductor: Conductor @EnvironmentObject var conductor: Conductor
@State var stopwatch: Stopwatch var body: some View {
@State var stopped: Bool = false
VStack {
ForEach(conductor.liveTimers) { liveTimer in
if let timer: AbstractTimer = liveTimer.timer(context: self.viewContext) {
LiveTimerView(timer: timer, date: liveTimer.date, endDate: liveTimer.endDate)
}
}
}.padding()
}
fileprivate func _columnCount() -> Int {
#if os(iOS)
if UIDevice.isPhoneIdiom {
return 18
} else {
return 3
}
#else
return 3
#endif
}
fileprivate func _columns() -> [GridItem] {
return (0..<self._columnCount()).map { _ in GridItem(spacing: 20.0) }
}
}
struct LiveTimerView: View {
var timer: AbstractTimer
var date: Date var date: Date
var endDate: Date?
var endDate: Date? { var body: some View {
return self.conductor.currentStopwatches[stopwatch.stringId]?.end
switch self.timer {
case let cd as Countdown:
LiveCountdownView(countdown: cd, date: self.date)
case let sw as Stopwatch:
LiveStopwatchView(stopwatch: sw, date: self.date, endDate: self.endDate)
default:
Text("unmanaged timer: \(timer)")
}
} }
}
struct LiveStopwatchView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var conductor: Conductor
@State var stopwatch: Stopwatch
var date: Date
var endDate: Date?
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if !self.stopped { if self.endDate == nil {
TimelineView(.periodic(from: self.date, by: 0.01)) { context in TimelineView(.periodic(from: self.date, by: 0.01)) { context in
TimeView(text: self._formattedDuration(date: context.date)) TimeView(text: self._formattedDuration(date: context.date))
} }
@ -57,7 +109,7 @@ struct LiveStopwatchView: View {
Spacer() Spacer()
Group { Group {
if self.stopped { if self.endDate != nil {
GreenCheckmarkView() GreenCheckmarkView()
} else { } else {
Image(systemName: "stop.circle.fill") Image(systemName: "stop.circle.fill")
@ -94,7 +146,6 @@ struct LiveStopwatchView: View {
} }
fileprivate func _stop() { fileprivate func _stop() {
self.stopped = true
Conductor.maestro.stopStopwatch(self.stopwatch) Conductor.maestro.stopStopwatch(self.stopwatch)
} }
} }
@ -176,53 +227,6 @@ struct LiveCountdownView: View {
} }
struct LiveTimerListView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var conductor: Conductor
var body: some View {
VStack {
ForEach(conductor.liveTimers) { liveTimer in
if let timer: AbstractTimer = liveTimer.timer(context: self.viewContext) {
switch timer {
case let cd as Countdown:
LiveCountdownView(countdown: cd, date: liveTimer.date)
case let sw as Stopwatch:
LiveStopwatchView(stopwatch: sw, date: liveTimer.date)
default:
Text("unmanaged timer: \(timer)")
}
}
}
}.padding()
}
fileprivate func _columnCount() -> Int {
#if os(iOS)
if UIDevice.isPhoneIdiom {
return 18
} else {
return 3
}
#else
return 3
#endif
}
fileprivate func _columns() -> [GridItem] {
return (0..<self._columnCount()).map { _ in GridItem(spacing: 20.0) }
}
}
struct LiveTimerView_Previews: PreviewProvider { struct LiveTimerView_Previews: PreviewProvider {

@ -18,5 +18,6 @@ struct LaunchWidgetAttributes: ActivityAttributes {
var id: String var id: String
var name: String var name: String
var date: Date var date: Date
var isTimer: Bool
} }

@ -62,7 +62,7 @@
"Don't show this again" = "Ne plus montrer"; "Don't show this again" = "Ne plus montrer";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Done" = "Sauver"; "Done" = "Terminer";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Total duration" = "Durée totale"; "Total duration" = "Durée totale";

Loading…
Cancel
Save