Live activities and various improvements

main
Laurent 3 years ago
parent c0731189ad
commit c1bdac59ff
  1. 31
      LaunchWidget/LaunchWidgetLiveActivity.swift
  2. 62
      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 {
ActivityConfiguration(for: LaunchWidgetAttributes.self) { context in
// let range = Date()...context.attributes.date
// Lock screen/banner UI goes here
HStack {
Text(context.attributes.name.uppercased())
Spacer()
Text(context.attributes.date, style: .timer)
.font(.title)
// if Date() < context.attributes.date {
// Text(context.attributes.date, style: .timer)
// } else {
// GreenCheckmarkView()
// }
// if context.attributes.endDate > self.model.now {
// Text(context.attributes.endDate, style: .timer)
// .monospaced()
// } else {
// Text("It's time!")
// }
if context.attributes.isTimer {
let range = Date()...context.attributes.date
Text(timerInterval: range,
pauseTime: range.lowerBound)
.font(.title)
} else {
Text(context.attributes.date, style: .timer)
.font(.title)
}
}.padding()
.monospaced()
.foregroundColor(.white)
.activityBackgroundTint(Color(white: 0.2))
.activityBackgroundTint(Color(red: 1.0, green: 0.5, blue: 0.4))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
@ -100,7 +93,7 @@ struct LaunchWidgetLiveActivity_Previews: PreviewProvider {
static let attributes = LaunchWidgetAttributes(
id: "",
name: "Tea",
date: Date().addingTimeInterval(3600.0))
date: Date().addingTimeInterval(3600.0), isTimer: true)
static let contentState = LaunchWidgetAttributes.ContentState(ended: false)

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

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

@ -42,10 +42,6 @@ import AVFoundation
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.volume = 1.0
self._player.delegate = self
@ -54,38 +50,18 @@ import AVFoundation
Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)")
self._player.play(atTime: self._player.deviceCurrentTime + duration)
// if repeatCount == 0 {
// self._scheduleFadeOut(duration: duration)
// }
}
func 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
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
Logger.log("audioPlayerDidFinishPlaying: successfully = \(flag)")
Conductor.maestro.cancelSoundPlayer(id: self._timerID)
// self._player.volume = 1.0
Conductor.maestro.cleanupLiveActivities()
}
}

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

@ -254,7 +254,7 @@ struct CountdownEditView : View {
fileprivate func _loadCountdown(_ countdown: Countdown) {
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)
if hours > 0 {

@ -23,27 +23,79 @@ struct TimeView: View {
}
struct LiveStopwatchView: View {
struct LiveTimerListView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var conductor: Conductor
@State var stopwatch: Stopwatch
@State var stopped: Bool = false
var body: some View {
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 endDate: Date?
var endDate: Date? {
return self.conductor.currentStopwatches[stopwatch.stringId]?.end
var body: some View {
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 {
HStack {
VStack(alignment: .leading) {
if !self.stopped {
if self.endDate == nil {
TimelineView(.periodic(from: self.date, by: 0.01)) { context in
TimeView(text: self._formattedDuration(date: context.date))
}
@ -57,7 +109,7 @@ struct LiveStopwatchView: View {
Spacer()
Group {
if self.stopped {
if self.endDate != nil {
GreenCheckmarkView()
} else {
Image(systemName: "stop.circle.fill")
@ -94,7 +146,6 @@ struct LiveStopwatchView: View {
}
fileprivate func _stop() {
self.stopped = true
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 {

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

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

Loading…
Cancel
Save