Store playlists as well as sounds

main
Laurent 3 years ago
parent 7a9ae3f7ff
commit ef1bfb7352
  1. 16
      LeCountdown.xcodeproj/project.pbxproj
  2. 6
      LeCountdown/Model/Generation/AbstractSoundTimer+CoreDataProperties.swift
  3. 2
      LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion
  4. 52
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.6.4.xcdatamodel/contents
  5. 48
      LeCountdown/Model/Model+Extensions.swift
  6. 21
      LeCountdown/Sound/Sound.swift
  7. 45
      LeCountdown/Utils/Extensions.swift
  8. 11
      LeCountdown/Views/Countdown/NewCountdownView.swift
  9. 21
      LeCountdown/Views/Reusable/TimerModel.swift

@ -132,6 +132,11 @@
C4A16DA929D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */; };
C4A16DAA29D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */; };
C4A16DAB29D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4A16DA629D0AAA800143D5E /* ESM_Ambient_Game_Menu_Soft_Wood_Confirm_1_Notification_Button_Settings_UI.wav */; };
C4A16DC529D311C800143D5E /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A16DC429D311C800143D5E /* Extensions.swift */; };
C4A16DC629D311C800143D5E /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A16DC429D311C800143D5E /* Extensions.swift */; };
C4A16DC729D311C800143D5E /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A16DC429D311C800143D5E /* Extensions.swift */; };
C4A16DC829D311C800143D5E /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A16DC429D311C800143D5E /* Extensions.swift */; };
C4A16DC929D311C800143D5E /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A16DC429D311C800143D5E /* Extensions.swift */; };
C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */; };
C4BA2ADB299549BC00CB4FBA /* TimerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */; };
C4BA2ADE2995ABA800CB4FBA /* MatriarchFxs_Loop2_Collider.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2ADD2995ABA800CB4FBA /* MatriarchFxs_Loop2_Collider.wav */; };
@ -391,6 +396,8 @@
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>"; };
C4A16DBD29D1C9DE00143D5E /* LeCountdown.0.6.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.3.xcdatamodel; sourceTree = "<group>"; };
C4A16DC429D311C800143D5E /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
C4A16DCA29D323CF00143D5E /* LeCountdown.0.6.4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.6.4.xcdatamodel; 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>"; };
C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerModel.swift; sourceTree = "<group>"; };
@ -700,6 +707,7 @@
C4BA2AFC299A3A3700CB4FBA /* AppleMusicPlayer.swift */,
C4742B5629840F6400D5D950 /* CoolPic.swift */,
C4BA2B6929A4BE1800CB4FBA /* Date+Extensions.swift */,
C4A16DC429D311C800143D5E /* Extensions.swift */,
C4BA2B5A299FFAB000CB4FBA /* Logger.swift */,
C4BA2B2C299E2DEE00CB4FBA /* Preferences.swift */,
C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */,
@ -1244,6 +1252,7 @@
C473C31829A926F50056B38A /* LaunchWidget.intentdefinition in Sources */,
C4E5D66A29B73FC6008E7465 /* TimerShortcuts.swift in Sources */,
C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */,
C4A16DC529D311C800143D5E /* Extensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1252,6 +1261,7 @@
buildActionMask = 2147483647;
files = (
C4A16D9629C4B06400143D5E /* StatePlayer.swift in Sources */,
C4A16DC629D311C800143D5E /* Extensions.swift in Sources */,
C4060DD7297AE73D003FAB80 /* LeCountdownTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1263,6 +1273,7 @@
C4060DE1297AE73D003FAB80 /* LeCountdownUITests.swift in Sources */,
C4A16D9729C4B06400143D5E /* StatePlayer.swift in Sources */,
C4060DE3297AE73D003FAB80 /* LeCountdownUITestsLaunchTests.swift in Sources */,
C4A16DC729D311C800143D5E /* Extensions.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1304,6 +1315,7 @@
C4F8B192298AC288005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C4BA2B18299BE6A000CB4FBA /* Interval+CoreDataClass.swift in Sources */,
C4F8B18E298AC288005C86A5 /* AbstractTimer+CoreDataProperties.swift in Sources */,
C4A16DC829D311C800143D5E /* Extensions.swift in Sources */,
C4F8B1AE298AC451005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C4BA2B32299F75DE00CB4FBA /* DefaultView.swift in Sources */,
C4F8B18D298AC288005C86A5 /* Stopwatch+CoreDataClass.swift in Sources */,
@ -1358,6 +1370,7 @@
C4BA2B4E299FCE0C00CB4FBA /* Stopwatch+CoreDataProperties.swift in Sources */,
C4E5D67829B88BB5008E7465 /* DelaySoundPlayer.swift in Sources */,
C4BA2B23299BE82E00CB4FBA /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C4A16DC929D311C800143D5E /* Extensions.swift in Sources */,
C473C2F529A8DAF30056B38A /* PropertyWrappers.swift in Sources */,
C473C2F029A8CFFC0056B38A /* TimerRouter.swift in Sources */,
C438C800298130E900BF3EF9 /* IntentDataProvider.swift in Sources */,
@ -1887,6 +1900,7 @@
C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
C4A16DCA29D323CF00143D5E /* LeCountdown.0.6.4.xcdatamodel */,
C4A16DBD29D1C9DE00143D5E /* LeCountdown.0.6.3.xcdatamodel */,
C4BA2B6B29A4C47100CB4FBA /* LeCountdown.0.6.2.xcdatamodel */,
C4BA2B46299FCD8B00CB4FBA /* LeCountdown.0.6.1.xcdatamodel */,
@ -1899,7 +1913,7 @@
C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */,
C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */,
);
currentVersion = C4A16DBD29D1C9DE00143D5E /* LeCountdown.0.6.3.xcdatamodel */;
currentVersion = C4A16DCA29D323CF00143D5E /* LeCountdown.0.6.4.xcdatamodel */;
path = LeCountdown.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

@ -2,7 +2,7 @@
// AbstractSoundTimer+CoreDataProperties.swift
// LeCountdown
//
// Created by Laurent Morvillier on 27/03/2023.
// Created by Laurent Morvillier on 28/03/2023.
//
//
@ -16,8 +16,8 @@ extension AbstractSoundTimer {
return NSFetchRequest<AbstractSoundTimer>(entityName: "AbstractSoundTimer")
}
@NSManaged public var repeatCount: Int16
@NSManaged public var soundList: String?
@NSManaged public var confirmationSoundList: String?
@NSManaged public var repeatCount: Int16
@NSManaged public var playableIds: String?
}

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>LeCountdown.0.6.3.xcdatamodel</string>
<string>LeCountdown.0.6.4.xcdatamodel</string>
</dict>
</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" elementID="soundList" syncable="YES">
<attribute name="confirmationSoundList" optional="YES" attributeType="String"/>
<attribute name="playableIds" optional="YES" attributeType="String" elementID="soundList"/>
<attribute name="repeatCount" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
</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>

@ -11,17 +11,31 @@ import CoreData
extension AbstractSoundTimer {
var sounds: Set<Sound> {
var playables: [any Playable] {
if let soundList {
return Set(soundList.enumItems())
var playables: [any Playable] = []
var playableIds = soundList.components(separatedBy: idSeparator)
for id in playableIds {
if let intId = numberFormatter.number(from: id)?.intValue,
let sound = Sound(rawValue: intId) {
playables.append(sound)
} else if let playlist = Playlist(rawValue: id) {
playables.append(playlist)
}
}
return playables
}
return []
}
func setSounds(_ sounds: Set<Sound>) {
self.soundList = sounds.stringRepresentation
var allSounds: Set<Sound> {
return self.playables.reduce(Set<Sound>()) { $0.union($1.soundList) }
}
// func setSounds(_ sounds: Set<Sound>) {
// self.soundList = sounds.stringRepresentation
// }
var confirmationSounds: Set<Sound> {
if let confirmationSoundList {
return Set(confirmationSoundList.enumItems())
@ -34,7 +48,7 @@ extension AbstractSoundTimer {
}
var someSound: Sound {
var sounds = self.sounds
var sounds = self.allSounds
// remove last played sound if the playlist has at least 3 sounds
if sounds.count > 2,
@ -130,27 +144,3 @@ extension CustomSound : Localized {
}
// MARK: - Storage convenience
fileprivate let separator = "|"
fileprivate let formatter: NumberFormatter = NumberFormatter()
extension String {
func enumItems<T : RawRepresentable<Int>>() -> [T] {
let ids: [String] = self.components(separatedBy: separator)
let intIds: [Int] = ids.compactMap { formatter.number(from: $0)?.intValue }
return intIds.compactMap { T(rawValue: $0) }
}
}
extension Sequence where Element : RawRepresentable<Int> {
var stringRepresentation: String {
let ids = self.compactMap { formatter.string(from: NSNumber(value: $0.rawValue)) }
return ids.joined(separator: separator)
}
}

@ -8,6 +8,23 @@
import Foundation
import AVFoundation
protocol Playable: StringRepresentable, Equatable, Hashable {
var soundList: Set<Sound> { get }
}
extension Playlist : Playable {
var stringValue: String { self.rawValue }
var soundList: Set<Sound> {
return Set(SoundCatalog.main.sounds(for: self))
}
}
extension Sound: Playable {
var stringValue: String { self.rawValue.formatted() }
var soundList: Set<Sound> {
return [self]
}
}
protocol Localized {
var localizedString: String { get }
}
@ -45,9 +62,9 @@ enum Catalog {
}
}
enum Playlist: Int, CaseIterable, Identifiable, Localized {
enum Playlist: String, CaseIterable, Identifiable, Localized {
var id: Int { return self.rawValue }
var id: String { return self.rawValue }
case custom
case nature

@ -0,0 +1,45 @@
//
// Extensions.swift
// LeCountdown
//
// Created by Laurent Morvillier on 28/03/2023.
//
import Foundation
// MARK: - Storage convenience
let idSeparator = "|"
let numberFormatter: NumberFormatter = NumberFormatter()
extension String {
func enumItems<T : RawRepresentable<Int>>() -> [T] {
let ids: [String] = self.components(separatedBy: idSeparator)
let intIds: [Int] = ids.compactMap { numberFormatter.number(from: $0)?.intValue }
return intIds.compactMap { T(rawValue: $0) }
}
}
//extension Sequence where Element : RawRepresentable<Int> {
// var stringRepresentation: String {
// let ids = self.compactMap { formatter.string(from: NSNumber(value: $0.rawValue)) }
// return ids.joined(separator: separator)
// }
//}
extension Sequence where Element : StringRepresentable {
var stringRepresentation: String {
let ids = self.compactMap { $0.stringValue }
return ids.joined(separator: idSeparator)
}
}
protocol StringRepresentable {
var stringValue: String { get }
}
extension String: StringRepresentable {
var stringValue: String { self }
}

@ -214,14 +214,12 @@ struct CountdownEditView : View {
self.nameString = name
}
self.model.soundModel.sounds = countdown.sounds
self.model.soundModel.setPlayables(countdown.playables)
// if let sound = Sound(rawValue: Int(countdown.sound)) {
// self.sound = sound
// }
self.soundRepeatCount = countdown.repeatCount
if let image = countdown.image, let coolpic = CoolPic(rawValue: image) {
if let image = countdown.image,
let coolpic = CoolPic(rawValue: image) {
self.image = coolpic
}
}
@ -262,7 +260,8 @@ struct CountdownEditView : View {
}
cd.image = self.image.rawValue
cd.setSounds(self.model.soundModel.sounds)
cd.playableIds = self.model.soundModel.playableIds
cd.setConfirmationSounds(self.model.confirmationSoundModel.sounds)
cd.repeatCount = self.soundRepeatCount

@ -32,6 +32,19 @@ class SoundModel: ObservableObject, SoundHolder {
}
}
func setPlayables(_ playables: [any Playable]) {
for playable in playables {
switch playable {
case let playlist as Playlist:
self.playlists.insert(playlist)
case let sound as Sound:
self.sounds.insert(sound)
default:
Logger.w("Unmanaged playable: \(playable)")
}
}
}
var soundSelection: String {
if !sounds.isEmpty {
if sounds.count == 1 {
@ -102,7 +115,13 @@ class SoundModel: ObservableObject, SoundHolder {
self.sounds.formSymmetricDifference(sounds)
}
}
}
var playableIds: String {
var ids: Set<String> = []
ids.formUnion(self.sounds.map { $0.stringValue} )
ids.formUnion(self.playlists.map { $0.stringValue} )
return ids.joined(separator: idSeparator)
}
}

Loading…
Cancel
Save