Adds confirmation sounds

main
Laurent 3 years ago
parent 79f817bf27
commit ea459d7a2a
  1. 14
      LeCountdown.xcodeproj/project.pbxproj
  2. 7
      LeCountdown/AppDelegate.swift
  3. 18
      LeCountdown/Conductor.swift
  4. 5
      LeCountdown/CountdownScheduler.swift
  5. 3
      LeCountdown/Model/Generation/AbstractSoundTimer+CoreDataProperties.swift
  6. 2
      LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion
  7. 52
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.3.xcdatamodel/contents
  8. 15
      LeCountdown/Model/Model+Extensions.swift
  9. 39
      LeCountdown/Sound/Sound.swift
  10. 0
      LeCountdown/Sound_Assets/Shorts/ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav
  11. 0
      LeCountdown/Sound_Assets/Shorts/ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav
  12. 0
      LeCountdown/Sound_Assets/Shorts/ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav
  13. 0
      LeCountdown/Sound_Assets/Shorts/MRKRSTPHR_synth_one_shot_bleep_G.wav
  14. 0
      LeCountdown/Sound_Assets/Shorts/PVP_Stab_Oneshot_Bleep_Em.wav
  15. 11
      LeCountdown/TimerRouter.swift
  16. 3
      LeCountdown/Views/Countdown/CountdownFormView.swift
  17. 8
      LeCountdown/Views/Countdown/NewCountdownView.swift
  18. 98
      LeCountdown/Views/Reusable/SoundFormView.swift
  19. 81
      LeCountdown/Views/Reusable/SoundSelectionView.swift
  20. 24
      LeCountdown/Views/Reusable/TimerModel.swift
  21. 6
      LeCountdown/Views/Stopwatch/StopwatchFormView.swift
  22. 1
      LeCountdown/fr.lproj/Localizable.strings

@ -397,6 +397,7 @@
C4A16DA029D0A7FE00143D5E /* ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav; sourceTree = "<group>"; }; C4A16DA029D0A7FE00143D5E /* ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav; sourceTree = "<group>"; };
C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav; sourceTree = "<group>"; }; C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav; sourceTree = "<group>"; };
C4A16DAC29D0AB1F00143D5E /* ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav; sourceTree = "<group>"; }; C4A16DAC29D0AB1F00143D5E /* ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav; sourceTree = "<group>"; };
C4A16DBD29D1C9DE00143D5E /* LeCountdown.0.6.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.3.xcdatamodel; sourceTree = "<group>"; };
C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundSelectionView.swift; sourceTree = "<group>"; }; C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundSelectionView.swift; sourceTree = "<group>"; };
C4BA2AD72993F7D200CB4FBA /* LeCountdown.0.5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.5.xcdatamodel; sourceTree = "<group>"; }; C4BA2AD72993F7D200CB4FBA /* LeCountdown.0.5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.5.xcdatamodel; sourceTree = "<group>"; };
C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerModel.swift; sourceTree = "<group>"; }; C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerModel.swift; sourceTree = "<group>"; };
@ -754,16 +755,24 @@
C445FA962987D0CF0054D761 /* Sound_Assets */ = { C445FA962987D0CF0054D761 /* Sound_Assets */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4A16DBC29D1A69200143D5E /* Shorts */,
C415D3EB29C376530037B215 /* Nature */, C415D3EB29C376530037B215 /* Nature */,
C415D3CC29C0B13A0037B215 /* Relax */, C415D3CC29C0B13A0037B215 /* Relax */,
C4BA2ADC2995AB7600CB4FBA /* Stephan_Bodzin */, C4BA2ADC2995AB7600CB4FBA /* Stephan_Bodzin */,
);
path = Sound_Assets;
sourceTree = "<group>";
};
C4A16DBC29D1A69200143D5E /* Shorts */ = {
isa = PBXGroup;
children = (
C4E5D68129B93583008E7465 /* PVP_Stab_Oneshot_Bleep_Em.wav */, C4E5D68129B93583008E7465 /* PVP_Stab_Oneshot_Bleep_Em.wav */,
C4A16DA029D0A7FE00143D5E /* ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav */, C4A16DA029D0A7FE00143D5E /* ESM_One_Shot_FX_Interface_Glitch_Spaceship_Console_18_Interface_Button_Alert_System_Cm.wav */,
C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */, C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */,
C4A16DAC29D0AB1F00143D5E /* ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav */, C4A16DAC29D0AB1F00143D5E /* ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav */,
C4A16D9A29D0A7D300143D5E /* MRKRSTPHR_synth_one_shot_bleep_G.wav */, C4A16D9A29D0A7D300143D5E /* MRKRSTPHR_synth_one_shot_bleep_G.wav */,
); );
path = Sound_Assets; path = Shorts;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C4BA2ADC2995AB7600CB4FBA /* Stephan_Bodzin */ = { C4BA2ADC2995AB7600CB4FBA /* Stephan_Bodzin */ = {
@ -1894,6 +1903,7 @@
C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = { C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
C4A16DBD29D1C9DE00143D5E /* LeCountdown.0.6.3.xcdatamodel */,
C4BA2B6B29A4C47100CB4FBA /* LeCountdown.0.6.2.xcdatamodel */, C4BA2B6B29A4C47100CB4FBA /* LeCountdown.0.6.2.xcdatamodel */,
C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */, C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */,
C4BA2B07299BDAE000CB4FBA /* LeCountdown.0.6.xcdatamodel */, C4BA2B07299BDAE000CB4FBA /* LeCountdown.0.6.xcdatamodel */,
@ -1905,7 +1915,7 @@
C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */, C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */,
C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */, C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */,
); );
currentVersion = C4BA2B6B29A4C47100CB4FBA /* LeCountdown.0.6.2.xcdatamodel */; currentVersion = C4A16DBD29D1C9DE00143D5E /* LeCountdown.0.6.3.xcdatamodel */;
path = LeCountdown.xcdatamodeld; path = LeCountdown.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel; versionGroupType = wrapper.xcdatamodel;

@ -81,8 +81,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
print("didReceive notification") print("didReceive notification")
let timerId = self._timerId(notificationId: response.notification.request.identifier) let timerId = self._timerId(notificationId: response.notification.request.identifier)
Conductor.maestro.cancelCountdown(id: timerId) // Conductor.maestro.cancelCountdown(id: timerId)
Conductor.maestro.cancelSoundPlayer(id: timerId)
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
@ -93,14 +93,13 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
Conductor.maestro.notifyUser(countdownId: timerId) Conductor.maestro.notifyUser(countdownId: timerId)
} }
fileprivate func _timerId(notificationId: String) -> String { fileprivate func _timerId(notificationId: String) -> TimerID {
let components = notificationId.components(separatedBy: CountdownScheduler.notificationIdSeparator) let components = notificationId.components(separatedBy: CountdownScheduler.notificationIdSeparator)
if components.count == 2 { if components.count == 2 {
return components[0] return components[0]
} else { } else {
fatalError("bad notification format : \(notificationId)") fatalError("bad notification format : \(notificationId)")
} }
} }
} }

@ -174,7 +174,8 @@ class Conductor: ObservableObject {
self.removeLiveTimer(id: countdown.stringId) self.removeLiveTimer(id: countdown.stringId)
let soundFile = try SoundFile(fullName: countdown.soundName) let sound = countdown.someSound
let soundFile = try SoundFile(fullName: sound.fileName)
let soundPlayer = try DelaySoundPlayer(timerID: countdown.stringId, soundFile: soundFile) let soundPlayer = try DelaySoundPlayer(timerID: countdown.stringId, soundFile: soundFile)
self._delayedSoundPlayers[countdown.stringId] = soundPlayer self._delayedSoundPlayers[countdown.stringId] = soundPlayer
try soundPlayer.start(in: countdown.duration, try soundPlayer.start(in: countdown.duration,
@ -184,7 +185,7 @@ class Conductor: ObservableObject {
self.currentCountdowns[countdown.stringId] = dateInterval self.currentCountdowns[countdown.stringId] = dateInterval
if Preferences.playConfirmationSound { if Preferences.playConfirmationSound {
self._playConfirmationSound() self._playConfirmationSound(timer: countdown)
} }
handler(.success(date)) handler(.success(date))
@ -234,7 +235,8 @@ class Conductor: ObservableObject {
if let countdown = context.object(stringId: countdownId) as? Countdown { if let countdown = context.object(stringId: countdownId) as? Countdown {
do { do {
let soundFile = try SoundFile(fullName: countdown.soundName) let sound: Sound = countdown.someSound
let soundFile: SoundFile = try SoundFile(fullName: sound.fileName)
let soundPlayer = try DelaySoundPlayer(timerID: countdownId, soundFile: soundFile) let soundPlayer = try DelaySoundPlayer(timerID: countdownId, soundFile: soundFile)
self._delayedSoundPlayers[countdown.stringId] = soundPlayer self._delayedSoundPlayers[countdown.stringId] = soundPlayer
try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount)) try soundPlayer.restore(for: interval.end, repeatCount: Int(countdown.repeatCount))
@ -288,8 +290,14 @@ class Conductor: ObservableObject {
AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate)) AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
} }
fileprivate func _playConfirmationSound() { fileprivate func _playConfirmationSound(timer: AbstractSoundTimer) {
self._playSound(Const.confirmationSound.rawValue) let fileName: String
if let confirmationSound = timer.confirmationSounds.randomElement() {
fileName = confirmationSound.fileName
} else {
fileName = Const.confirmationSound.rawValue
}
self._playSound(fileName)
} }
fileprivate func _playCancellationSound() { fileprivate func _playCancellationSound() {

@ -36,9 +36,7 @@ class CountdownScheduler {
content.body = body content.body = body
let sound = countdown.soundName
self._createNotification(countdown: countdown, content: content, handler: handler) self._createNotification(countdown: countdown, content: content, handler: handler)
print("Selected sound = \(sound)")
// content.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: 1.0) // content.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: 1.0)
content.interruptionLevel = .critical content.interruptionLevel = .critical
@ -92,13 +90,10 @@ class CountdownScheduler {
} }
func cancelCurrentNotifications(countdownId: String) { func cancelCurrentNotifications(countdownId: String) {
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(countdownId) } let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(countdownId) }
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids) UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
} }
// Conductor.maestro.cancelCountdown(id: countdownId)
} }
} }

@ -2,7 +2,7 @@
// AbstractSoundTimer+CoreDataProperties.swift // AbstractSoundTimer+CoreDataProperties.swift
// LeCountdown // LeCountdown
// //
// Created by Laurent Morvillier on 10/02/2023. // Created by Laurent Morvillier on 27/03/2023.
// //
// //
@ -18,5 +18,6 @@ extension AbstractSoundTimer {
@NSManaged public var repeatCount: Int16 @NSManaged public var repeatCount: Int16
@NSManaged public var soundList: String? @NSManaged public var soundList: String?
@NSManaged public var confirmationSoundList: String?
} }

@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>LeCountdown.0.6.2.xcdatamodel</string> <string>LeCountdown.0.6.3.xcdatamodel</string>
</dict> </dict>
</plist> </plist>

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="AbstractSoundTimer" representedClassName="AbstractSoundTimer" isAbstract="YES" parentEntity="AbstractTimer" syncable="YES">
<attribute name="confirmationSoundList" optional="YES" attributeType="String"/>
<attribute name="repeatCount" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="soundList" optional="YES" attributeType="String"/>
</entity>
<entity name="AbstractTimer" representedClassName="AbstractTimer" isAbstract="YES" syncable="YES">
<attribute name="image" optional="YES" attributeType="String"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="timers" inverseEntity="Activity"/>
</entity>
<entity name="Activity" representedClassName="Activity" syncable="YES">
<attribute name="name" attributeType="String" defaultValueString=""/>
<relationship name="records" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Record" inverseName="activity" inverseEntity="Record"/>
<relationship name="timers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AbstractTimer" inverseName="activity" inverseEntity="AbstractTimer"/>
</entity>
<entity name="Alarm" representedClassName="Alarm" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="fireDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="Countdown" representedClassName="Countdown" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="group" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="IntervalGroup" inverseName="countdown" inverseEntity="IntervalGroup"/>
</entity>
<entity name="CustomSound" representedClassName="CustomSound" syncable="YES">
<attribute name="file" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
</entity>
<entity name="Interval" representedClassName="Interval" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="soundList" optional="YES" attributeType="String"/>
<relationship name="group" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="IntervalGroup" inverseName="intervals" inverseEntity="IntervalGroup"/>
</entity>
<entity name="IntervalGroup" representedClassName="IntervalGroup" syncable="YES">
<attribute name="repeatCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="countdown" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Countdown" inverseName="group" inverseEntity="Countdown"/>
<relationship name="intervals" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Interval" inverseName="group" inverseEntity="Interval"/>
</entity>
<entity name="Record" representedClassName="Record" syncable="YES">
<attribute name="duration" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="end" attributeType="Date" defaultDateTimeInterval="696425400" usesScalarValueType="NO"/>
<attribute name="month" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="start" attributeType="Date" defaultDateTimeInterval="696425400" usesScalarValueType="NO"/>
<attribute name="year" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="records" inverseEntity="Activity"/>
</entity>
<entity name="Stopwatch" representedClassName="Stopwatch" parentEntity="AbstractSoundTimer" syncable="YES">
<attribute name="end" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sound" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
</model>

@ -22,6 +22,17 @@ extension AbstractSoundTimer {
self.soundList = sounds.stringRepresentation self.soundList = sounds.stringRepresentation
} }
var confirmationSounds: Set<Sound> {
if let confirmationSoundList {
return Set(confirmationSoundList.enumItems())
}
return []
}
func setConfirmationSounds(_ sounds: Set<Sound>) {
self.confirmationSoundList = sounds.stringRepresentation
}
var someSound: Sound { var someSound: Sound {
var sounds = self.sounds var sounds = self.sounds
@ -40,10 +51,6 @@ extension AbstractSoundTimer {
return Sound.default return Sound.default
} }
var soundName: String {
return self.someSound.fileName
}
} }
extension Stopwatch { extension Stopwatch {

@ -23,37 +23,50 @@ class SoundCatalog {
} }
func sounds(for playlist: Playlist) -> [Sound] { func sounds(for playlist: Playlist) -> [Sound] {
return self._soundsByPlaylist[playlist] ?? [] switch playlist {
case .shorts:
return [.FF_SH_bowl_drone_tap_hold_E, .FF_SH_bowl_drone_tapping_C, .EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab, .ESM_Ambient_Game_Menu_Soft_Wood]
default:
return self._soundsByPlaylist[playlist] ?? []
}
} }
} }
enum Catalog {
case ring
case confirmation
var playlists: [Playlist] {
switch self {
case .ring: return [.nature, .stephanBodzin, .relax]
case .confirmation: return [.shorts]
}
}
}
enum Playlist: Int, CaseIterable, Identifiable, Localized { enum Playlist: Int, CaseIterable, Identifiable, Localized {
var id: Int { return self.rawValue } var id: Int { return self.rawValue }
case custom case custom
case nature case nature
// case fun
case stephanBodzin case stephanBodzin
case relax case relax
case shorts
static var selectable: [Playlist] {
return Playlist.allCases.filter { $0 != .custom }
}
var localizedString: String { var localizedString: String {
switch self { switch self {
case .nature: case .nature:
return NSLocalizedString("Nature", comment: "") return NSLocalizedString("Nature", comment: "")
// case .fun:
// return NSLocalizedString("Fun", comment: "")
case .stephanBodzin: case .stephanBodzin:
return "Stephan Bodzin" return "Stephan Bodzin"
case .custom: case .custom:
return NSLocalizedString("Custom", comment: "") return NSLocalizedString("Custom", comment: "")
case .relax: case .relax:
return NSLocalizedString("Relax", comment: "") return NSLocalizedString("Relax", comment: "")
case .shorts:
return NSLocalizedString("Confirmation", comment: "")
} }
} }
@ -75,12 +88,12 @@ enum Sound: Int, CaseIterable, Identifiable, Localized {
// Relax // Relax
case FF_SH_bowl_drone_tapping_C case FF_SH_bowl_drone_tapping_C
case FF_SH_bowl_drone_tap_hold_E case FF_SH_bowl_drone_tap_hold_E
case EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab
case EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm case EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm
case EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am
case EX_ATSM_Bell_Binaural_Flam_Eb case EX_ATSM_Bell_Binaural_Flam_Eb
case EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am case EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am
case EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm case EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm
case EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am
case EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab
// Nature // Nature
case rain_soft case rain_soft
case stream1 case stream1
@ -91,6 +104,8 @@ enum Sound: Int, CaseIterable, Identifiable, Localized {
case deciduousForestMorning case deciduousForestMorning
case wetland case wetland
case riparianZone case riparianZone
// Shorts
case ESM_Ambient_Game_Menu_Soft_Wood
static var `default`: Sound { .sbSEM_Synths_Loop4_Nothing_Like_You } static var `default`: Sound { .sbSEM_Synths_Loop4_Nothing_Like_You }
@ -120,6 +135,7 @@ enum Sound: Int, CaseIterable, Identifiable, Localized {
case .deciduousForestMorning: return "Forest morning 2" case .deciduousForestMorning: return "Forest morning 2"
case .wetland: return "Wetland" case .wetland: return "Wetland"
case .riparianZone: return "Riparian Zone" case .riparianZone: return "Riparian Zone"
case .ESM_Ambient_Game_Menu_Soft_Wood: return "Wood percussion"
} }
} }
@ -149,6 +165,7 @@ enum Sound: Int, CaseIterable, Identifiable, Localized {
case .deciduousForestMorning: return "QP01 0050 Deciduous forest morning songbirds robin.wav" case .deciduousForestMorning: return "QP01 0050 Deciduous forest morning songbirds robin.wav"
case .wetland: return "QP01 0096 Wetland lake early morning.wav" case .wetland: return "QP01 0096 Wetland lake early morning.wav"
case .riparianZone: return "QP01 0096 Wetland lake early morning.wav" case .riparianZone: return "QP01 0096 Wetland lake early morning.wav"
case .ESM_Ambient_Game_Menu_Soft_Wood: return "ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav"
} }
} }
@ -160,6 +177,8 @@ enum Sound: Int, CaseIterable, Identifiable, Localized {
return .relax return .relax
case .rain_soft, .stream1, .stream2, .surf1, .crickets, .tropicalForestMorning, .deciduousForestMorning, .wetland, .riparianZone: case .rain_soft, .stream1, .stream2, .surf1, .crickets, .tropicalForestMorning, .deciduousForestMorning, .wetland, .riparianZone:
return .nature return .nature
case .ESM_Ambient_Game_Menu_Soft_Wood:
return .shorts
} }
} }

@ -45,17 +45,6 @@ class TimerRouter {
} }
static func stopTimer(timer: AbstractTimer) {
switch timer {
case let countdown as Countdown:
Conductor.maestro.cancelCountdown(id: countdown.stringId)
case let stopwatch as Stopwatch:
self._stopStopwatch(stopwatch)
default:
print("missing launcher for \(self)")
}
}
fileprivate static func _launchCountdown(_ countdown: Countdown, handler: @escaping (Result<Void, Error>) -> Void) { fileprivate static func _launchCountdown(_ countdown: Countdown, handler: @escaping (Result<Void, Error>) -> Void) {
UNUserNotificationCenter.current().getNotificationSettings { settings in UNUserNotificationCenter.current().getNotificationSettings { settings in

@ -19,6 +19,7 @@ struct CountdownFormView : View {
@FocusState private var focusedField: CountdownField? @FocusState private var focusedField: CountdownField?
@EnvironmentObject var model: TimerModel @EnvironmentObject var model: TimerModel
@EnvironmentObject var confirmationModel: TimerModel
var secondsBinding: Binding<String> var secondsBinding: Binding<String>
var minutesBinding: Binding<String> var minutesBinding: Binding<String>
@ -55,9 +56,9 @@ struct CountdownFormView : View {
} }
SoundFormView( SoundFormView(
model: self.model,
imageBinding: imageBinding, imageBinding: imageBinding,
repeatCountBinding: repeatCountBinding) repeatCountBinding: repeatCountBinding)
.environmentObject(self.model)
}.toolbar { }.toolbar {
ToolbarItemGroup(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {
Button { Button {

@ -193,7 +193,7 @@ struct CountdownEditView : View {
} }
self.model.group = preset.intervalGroup self.model.group = preset.intervalGroup
self.model.sounds = preset.sound self.model.soundModel.sounds = preset.sound
} }
@ -214,7 +214,7 @@ struct CountdownEditView : View {
self.nameString = name self.nameString = name
} }
self.model.sounds = countdown.sounds self.model.soundModel.sounds = countdown.sounds
// if let sound = Sound(rawValue: Int(countdown.sound)) { // if let sound = Sound(rawValue: Int(countdown.sound)) {
// self.sound = sound // self.sound = sound
@ -262,9 +262,9 @@ struct CountdownEditView : View {
} }
cd.image = self.image.rawValue cd.image = self.image.rawValue
cd.setSounds(self.model.sounds) cd.setSounds(self.model.soundModel.sounds)
cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds)
// cd.setPlaylists(self.playlists)
cd.repeatCount = self.soundRepeatCount cd.repeatCount = self.soundRepeatCount
if !self.nameString.isEmpty { if !self.nameString.isEmpty {

@ -9,9 +9,9 @@ import SwiftUI
struct SoundFormView : View { struct SoundFormView : View {
var imageBinding: Binding<CoolPic> var model: TimerModel
@EnvironmentObject var model: TimerModel var imageBinding: Binding<CoolPic>
var repeatCountBinding: Binding<Int16>? = nil var repeatCountBinding: Binding<Int16>? = nil
var optionalSound: Binding<Bool>? = nil var optionalSound: Binding<Bool>? = nil
@ -25,26 +25,13 @@ struct SoundFormView : View {
Section(header: Text("Properties")) { Section(header: Text("Properties")) {
if self.optionalSound != nil { if self.optionalSound != nil {
Toggle("Play sound on end", isOn: optionalSound!) Toggle("Play sound on end", isOn: optionalSound!)
if self.optionalSound?.wrappedValue == true { if self.optionalSound?.wrappedValue == true {
NavigationLink { NavigationLink {
PlaylistsView(model: self.model.soundModel, catalog: .ring)
PlaylistsView().environmentObject(self.model)
// SoundSelectionView(playlistBinding: playlistBinding)
} label: { } label: {
Text("Sound") Text("Sound")
} }
// Picker(selection: soundBinding) {
// ForEach(Sound.allCases) { sound in
// Text(sound.localizedString).tag(sound)
// }
// } label: {
// Text("Sound")
// }
} }
} else { } else {
@ -57,17 +44,21 @@ struct SoundFormView : View {
// } // }
} }
NavigationLink { SoundLinkView(soundModel: self.model.soundModel,
NavigationStack { catalog: .ring,
PlaylistsView().environmentObject(self.model) title: "Sound")
}
} label: { // NavigationLink {
HStack { // NavigationStack {
Text("Sound") // PlaylistsView(model: self.model.soundModel, catalog: .ring)
Spacer() // }
Text(self.model.soundSelection) // } label: {
} // HStack {
} // Text("Sound")
// Spacer()
// Text(self.model.soundModel.soundSelection)
// }
// }
if self.repeatCountBinding != nil { if self.repeatCountBinding != nil {
Picker("Repeat Count", selection: self.repeatCountBinding!) { Picker("Repeat Count", selection: self.repeatCountBinding!) {
@ -79,28 +70,23 @@ struct SoundFormView : View {
} }
} }
} SoundLinkView(soundModel: self.model.confirmationSoundModel,
catalog: .confirmation,
title: "Confirmation Sound")
// Section(header: Text("Background")) { // NavigationLink {
// // NavigationStack {
// Button { // PlaylistsView(model: self.model.confirmationSoundModel, catalog: .confirmation)
// self.imageSelectionSheetShown = true // }
// } label: { // } label: {
// Group { // HStack {
// if let image = self.imageBinding.wrappedValue { // Text("Confirmation Sound")
// Image(image.rawValue).resizable() // Spacer()
// } else { // Text(self.confirmationModel.soundSelection)
// Image(imageBinding.wrappedValue.rawValue).resizable()
// }
// } // }
// .font(Font.system(size: 90.0))
// .aspectRatio(1, contentMode: .fit)
// .frame(width: 100.0, height: 100.0)
// .cornerRadius(20.0)
//
// } // }
//
// } }
}.sheet(isPresented: self.$imageSelectionSheetShown) { }.sheet(isPresented: self.$imageSelectionSheetShown) {
ImageSelectionView(showBinding: self.$imageSelectionSheetShown, imageBinding: self.imageBinding) ImageSelectionView(showBinding: self.$imageSelectionSheetShown, imageBinding: self.imageBinding)
} }
@ -108,14 +94,32 @@ struct SoundFormView : View {
} }
struct SoundLinkView: View {
@StateObject var soundModel: SoundModel
var catalog: Catalog
var title: String
var body: some View {
NavigationLink {
NavigationStack {
PlaylistsView(model: self.soundModel,
catalog: self.catalog)
}
} label: {
LabeledContent(self.title, value: self.soundModel.soundSelection)
}
}
}
struct SoundImageFormView_Previews: PreviewProvider { struct SoundImageFormView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Form { Form {
SoundFormView( SoundFormView(model: TimerModel(),
imageBinding: .constant(.pic1), imageBinding: .constant(.pic1),
repeatCountBinding: .constant(2)) repeatCountBinding: .constant(2))
.environmentObject(TimerModel())
} }
} }
} }

@ -9,14 +9,15 @@ import SwiftUI
struct PlaylistsView: View { struct PlaylistsView: View {
@EnvironmentObject var model: TimerModel @StateObject var model: SoundModel
var catalog: Catalog
var body: some View { var body: some View {
Form { Form {
ForEach(Playlist.selectable) { playlist in ForEach(catalog.playlists) { playlist in
PlaylistSectionView(playlist: playlist) PlaylistSectionView(model: self.model, playlist: playlist)
.environmentObject(self.model)
} }
} }
.navigationTitle("Sounds") .navigationTitle("Sounds")
@ -26,7 +27,7 @@ struct PlaylistsView: View {
struct PlaylistSectionView: View { struct PlaylistSectionView: View {
@EnvironmentObject var model: TimerModel @StateObject var model: SoundModel
var playlist: Playlist var playlist: Playlist
@ -101,15 +102,6 @@ struct RightAlignToggleRow<T : Localized>: View {
} }
}.onChange(of: self.selected, perform: handleSelection) }.onChange(of: self.selected, perform: handleSelection)
// if let keyPath {
//
//
// Toggle(item[keyPath: keyPath], isOn: $selected)
// .onChange(of: self.selected, perform: handleSelection)
// } else {
// Toggle(item.localizedString, isOn: $selected)
// .onChange(of: self.selected, perform: handleSelection)
// }
} }
} }
@ -129,66 +121,10 @@ struct ImageToggleRow<T : Localized>: View {
} }
//struct PlaylistRow: View {
// var playlist: Playlist
// @State var selected: Bool
// var handleSelection: (Bool) -> ()
//
// var body: some View {
// Toggle(playlist.localizedString, isOn: $selected)
// .onChange(of: selected, perform: handleSelection)
// }
//}
//
//struct SoundRow: View {
// var sound: Sound
// @State var selected: Bool
// var handleSelection: (Bool) -> ()
//
// var body: some View {
// Toggle(sound.localizedString, isOn: $selected)
// .onChange(of: selected, perform: handleSelection)
// }
//}
struct SoundSelectionView: View {
var body: some View {
Form {
// PlaylistsView()
// ForEach($playlistBinding, id: \.id) { $ps in
//
//
// Section {
// ForEach(ps.playlist.sounds) { sound in
//
// SoundRow(sound: sound, selected: ps.sounds.contains(sound)) { selected in
// if selected {
// ps.sounds.append(sound)
// } else {
// ps.sounds.removeAll(where: { $0 == sound })
// }
// }
// }
// } header: {
// Toggle(ps.playlist.localizedString, isOn: $ps.selected)
// }
// }.navigationTitle("Sounds")
}
}
}
struct PlaylistsView_Previews: PreviewProvider { struct PlaylistsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PlaylistsView() PlaylistsView(model: SoundModel(), catalog: .ring)
.environmentObject(TimerModel())
} }
} }
@ -196,8 +132,7 @@ struct PlaylistSectionView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Form { Form {
PlaylistSectionView(playlist: .stephanBodzin) PlaylistSectionView(model: SoundModel(), playlist: .stephanBodzin)
.environmentObject(TimerModel())
} }
} }
} }

@ -13,7 +13,17 @@ protocol SoundHolder {
func selectPlaylist(_ playlist: Playlist, selected: Bool) func selectPlaylist(_ playlist: Playlist, selected: Bool)
} }
class TimerModel : ObservableObject, SoundHolder { class TimerModel: ObservableObject {
@Published var soundModel: SoundModel = SoundModel()
@Published var confirmationSoundModel: SoundModel = SoundModel()
@Published var group: CountdownIntervalGroup =
CountdownIntervalGroup(repeatCount: 0, intervals: [])
}
class SoundModel: ObservableObject, SoundHolder {
@Published var playlists: Set<Playlist> = [] @Published var playlists: Set<Playlist> = []
@Published var sounds: Set<Sound> = [] { @Published var sounds: Set<Sound> = [] {
@ -21,8 +31,6 @@ class TimerModel : ObservableObject, SoundHolder {
self._selectPlaylists() self._selectPlaylists()
} }
} }
@Published var group: CountdownIntervalGroup =
CountdownIntervalGroup(repeatCount: 0, intervals: [])
var soundSelection: String { var soundSelection: String {
if !sounds.isEmpty { if !sounds.isEmpty {
@ -56,13 +64,11 @@ class TimerModel : ObservableObject, SoundHolder {
// MARK: - SoundHolder // MARK: - SoundHolder
func selectSound(_ sound: Sound, selected: Bool) { func selectSound(_ sound: Sound, selected: Bool) {
if selected { if selected {
self.sounds.insert(sound) self.sounds.insert(sound)
} else { } else {
self.sounds.remove(sound) self.sounds.remove(sound)
} }
self._togglePlaylist(sound.playlist) self._togglePlaylist(sound.playlist)
} }
@ -99,12 +105,4 @@ class TimerModel : ObservableObject, SoundHolder {
} }
// func isSelected(sound: Sound) -> Bool {
// self.sounds.contains(sound)
// }
//
// func isSelected(playlist: Playlist) -> Bool {
// self.playlists.contains(playlist)
// }
} }

@ -34,9 +34,9 @@ struct StopwatchFormView: View {
} }
} }
SoundFormView(imageBinding: imageBinding, SoundFormView(model: self.model,
optionalSound: playSoundBinding) imageBinding: imageBinding,
.environmentObject(self.model) optionalSound: playSoundBinding)
}.toolbar { }.toolbar {
ToolbarItemGroup(placement: .keyboard) { ToolbarItemGroup(placement: .keyboard) {

@ -254,3 +254,4 @@
"Play confirmation sound" = "Jouer son de confirmation"; "Play confirmation sound" = "Jouer son de confirmation";
"Play cancellation sound" = "Jouer son d'annulation"; "Play cancellation sound" = "Jouer son d'annulation";
"Contact us" = "Contactez-nous"; "Contact us" = "Contactez-nous";
"Confirmation" = "Confirmation";

Loading…
Cancel
Save