Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
DDCC3A5A2DDC988F006F1C10 /* CarbSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A592DDC988F006F1C10 /* CarbSample.swift */; };
DDCC3A5B2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */; };
DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */; };
DDCF9AA02E99000000000001 /* CustomSoundStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9AA02E99000000000002 /* CustomSoundStore.swift */; };
DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A812D85FD14004DF4DD /* AlarmType.swift */; };
DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A872D85FD33004DF4DD /* AlarmData.swift */; };
DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */; };
Expand Down Expand Up @@ -716,6 +717,7 @@
DDCC3ABF2DDE10B0006F1C10 /* Testing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Testing.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/Testing.framework; sourceTree = DEVELOPER_DIR; };
DDCC3AD62DDE1790006F1C10 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = "<group>"; };
DDCF9AA02E99000000000002 /* CustomSoundStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSoundStore.swift; sourceTree = "<group>"; };
DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = "<group>"; };
DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = "<group>"; };
DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1374,6 +1376,7 @@
DDCF9A872D85FD33004DF4DD /* AlarmData.swift */,
DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */,
DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */,
DDCF9AA02E99000000000002 /* CustomSoundStore.swift */,
);
path = Alarm;
sourceTree = "<group>";
Expand Down Expand Up @@ -2422,6 +2425,7 @@
DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */,
379BECAA2F6588300069DC62 /* RestartLiveActivityIntent.swift in Sources */,
DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */,
DDCF9AA02E99000000000001 /* CustomSoundStore.swift in Sources */,
DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */,
DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */,
DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Alarm/Alarm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ struct Alarm: Identifiable, Codable, Equatable {
AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze")

if playSound {
AlarmSound.setSoundFile(str: soundFile.rawValue)
AlarmSound.setSoundFile(soundFile)
// Only use delay if repeating is enabled, otherwise delay doesn't make sense
let delay = shouldRepeat ? soundDelay : 0
AlarmSound.play(repeating: shouldRepeat, delay: delay)
Expand Down
111 changes: 97 additions & 14 deletions LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// AlarmAudioSection.swift

import SwiftUI
import UniformTypeIdentifiers

struct AlarmAudioSection: View {
@Binding var alarm: Alarm
Expand Down Expand Up @@ -111,27 +112,37 @@ private struct TonePickerSheet: View {
@Binding var selected: SoundFile
@Environment(\.dismiss) private var dismiss

@State private var customSounds: [CustomSound] = []
@State private var showingImporter = false
@State private var importError: String?

var body: some View {
NavigationView {
ScrollViewReader { proxy in
List {
ForEach(SoundFile.allCases) { tone in
Section {
ForEach(customSounds) { sound in
toneRow(tone: .custom(sound.id), label: sound.displayName)
.id(SoundFile.custom(sound.id))
}
.onDelete(perform: deleteCustomSounds)

Button {
selected = tone
AlarmSound.setSoundFile(str: tone.rawValue)
AlarmSound.stop()
AlarmSound.playTest()
showingImporter = true
} label: {
HStack {
Text(tone.displayName)
if tone == selected {
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
Label("Import Sound…", systemImage: "plus.circle")
}
} header: {
Text("Custom")
} footer: {
Text("Custom sounds stay on this device and aren't included in settings export.")
}

Section(header: Text("Built-in")) {
ForEach(SoundFile.allBuiltins) { tone in
toneRow(tone: tone, label: tone.displayName)
.id(tone)
}
.id(tone)
}
}
.navigationTitle("Choose Tone")
Expand All @@ -144,9 +155,81 @@ private struct TonePickerSheet: View {
}
}
.onAppear {
reloadCustomSounds()
proxy.scrollTo(selected, anchor: .center)
}
.fileImporter(
isPresented: $showingImporter,
allowedContentTypes: [.audio],
allowsMultipleSelection: false
) { result in
handleImport(result)
}
.alert(
"Import Failed",
isPresented: Binding(
get: { importError != nil },
set: { if !$0 { importError = nil } }
),
actions: { Button("OK", role: .cancel) { importError = nil } },
message: { Text(importError ?? "") }
)
}
}
}

@ViewBuilder
private func toneRow(tone: SoundFile, label: String) -> some View {
Button {
selected = tone
AlarmSound.setSoundFile(tone)
AlarmSound.stop()
AlarmSound.playTest()
} label: {
HStack {
Text(label)
if tone == selected {
Spacer()
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}

private func reloadCustomSounds() {
customSounds = CustomSoundStore.shared.list()
}

private func deleteCustomSounds(at offsets: IndexSet) {
let deleted = offsets.map { customSounds[$0] }
for sound in deleted {
CustomSoundStore.shared.delete(sound.id)
}
// If the alarm was pointing at a sound we just deleted, fall back to a valid
// built-in so the picker and the alarm don't keep a dangling reference.
if deleted.contains(where: { SoundFile.custom($0.id) == selected }) {
selected = .fallback
}
reloadCustomSounds()
}

private func handleImport(_ result: Result<[URL], Error>) {
switch result {
case let .success(urls):
guard let url = urls.first else { return }
do {
let imported = try CustomSoundStore.shared.importFile(at: url)
reloadCustomSounds()
selected = .custom(imported.id)
AlarmSound.setSoundFile(.custom(imported.id))
AlarmSound.stop()
AlarmSound.playTest()
} catch {
importError = error.localizedDescription
}
case let .failure(error):
importError = error.localizedDescription
}
}
}
Loading
Loading