Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6d3837022d
commit
706aba5d30
@ -10,10 +10,10 @@ import Foundation
|
||||
/// Configuration model for sound system loaded from JSON
|
||||
public struct SoundConfiguration: Codable {
|
||||
public let sounds: [Sound]
|
||||
public let categories: [SoundCategory]
|
||||
public let categories: [SoundCategory]?
|
||||
public let settings: AudioSettings
|
||||
|
||||
public init(sounds: [Sound], categories: [SoundCategory], settings: AudioSettings) {
|
||||
public init(sounds: [Sound], categories: [SoundCategory]? = nil, settings: AudioSettings) {
|
||||
self.sounds = sounds
|
||||
self.categories = categories
|
||||
self.settings = settings
|
||||
@ -109,7 +109,7 @@ public class SoundConfigurationService {
|
||||
|
||||
/// Get available categories
|
||||
public func getAvailableCategories() -> [SoundCategory] {
|
||||
return getConfiguration().categories
|
||||
return getConfiguration().categories ?? []
|
||||
}
|
||||
|
||||
/// Get audio settings
|
||||
|
||||
100
TheNoiseClock/Models/SoundCategory.swift
Normal file
100
TheNoiseClock/Models/SoundCategory.swift
Normal file
@ -0,0 +1,100 @@
|
||||
//
|
||||
// SoundCategory.swift
|
||||
// TheNoiseClock
|
||||
//
|
||||
// Created by Matt Bruce on 9/8/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Enum representing sound categories with associated icons, display names, and metadata
|
||||
public enum SoundCategory: String, CaseIterable, Identifiable {
|
||||
case all = "all"
|
||||
case colored = "colored"
|
||||
case ambient = "ambient"
|
||||
case nature = "nature"
|
||||
case mechanical = "mechanical"
|
||||
case alarm = "alarm"
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
/// Display name for the category
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .all: return "All"
|
||||
case .colored: return "Colored"
|
||||
case .ambient: return "Ambient"
|
||||
case .nature: return "Nature"
|
||||
case .mechanical: return "Mechanical"
|
||||
case .alarm: return "Alarm Sounds"
|
||||
}
|
||||
}
|
||||
|
||||
/// SF Symbol icon name for the category
|
||||
public var icon: String {
|
||||
switch self {
|
||||
case .all: return "speaker.wave.2"
|
||||
case .colored: return "waveform.path"
|
||||
case .ambient: return "waveform"
|
||||
case .nature: return "cloud.rain"
|
||||
case .mechanical: return "fan"
|
||||
case .alarm: return "alarm"
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundle name for organizing audio files
|
||||
public var bundleName: String? {
|
||||
switch self {
|
||||
case .all: return nil
|
||||
case .colored: return "Colored"
|
||||
case .ambient: return "Ambient"
|
||||
case .nature: return "Nature"
|
||||
case .mechanical: return "Mechanical"
|
||||
case .alarm: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of the category
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .all: return "All available sounds"
|
||||
case .colored: return "Synthetic noise signals for focus, sleep, and relaxation"
|
||||
case .ambient: return "General ambient sounds"
|
||||
case .nature: return "Natural environmental sounds"
|
||||
case .mechanical: return "Mechanical and electronic sounds"
|
||||
case .alarm: return "Wake-up and notification alarm sounds"
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferred sort order for categories (lower number = appears first)
|
||||
public var sortOrder: Int {
|
||||
switch self {
|
||||
case .all: return 0
|
||||
case .colored: return 1
|
||||
case .ambient: return 2
|
||||
case .nature: return 3
|
||||
case .mechanical: return 4
|
||||
case .alarm: return 5
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize from string, returning nil if invalid
|
||||
public init?(from string: String) {
|
||||
self.init(rawValue: string)
|
||||
}
|
||||
|
||||
/// Get all non-alarm categories for noise selection
|
||||
public static var noiseCategories: [SoundCategory] {
|
||||
return [.all, .colored, .ambient, .nature, .mechanical]
|
||||
}
|
||||
|
||||
/// Get all categories sorted by preferred order
|
||||
public static var sortedCategories: [SoundCategory] {
|
||||
return allCases.sorted { $0.sortOrder < $1.sortOrder }
|
||||
}
|
||||
|
||||
/// Get noise categories sorted by preferred order
|
||||
public static var sortedNoiseCategories: [SoundCategory] {
|
||||
return noiseCategories.sorted { $0.sortOrder < $1.sortOrder }
|
||||
}
|
||||
}
|
||||
BIN
TheNoiseClock/Resources/Ambient.bundle/ambient-waves.mp3
Normal file
BIN
TheNoiseClock/Resources/Ambient.bundle/ambient-waves.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Ambient.bundle/atmospheric-pad.mp3
Normal file
BIN
TheNoiseClock/Resources/Ambient.bundle/atmospheric-pad.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Ambient.bundle/calm-ambient-pad.mp3
Normal file
BIN
TheNoiseClock/Resources/Ambient.bundle/calm-ambient-pad.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Ambient.bundle/dark-ambient.mp3
Normal file
BIN
TheNoiseClock/Resources/Ambient.bundle/dark-ambient.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Ambient.bundle/ethereal-ambient.mp3
Normal file
BIN
TheNoiseClock/Resources/Ambient.bundle/ethereal-ambient.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Colored.bundle/brown-noise.mp3
Normal file
BIN
TheNoiseClock/Resources/Colored.bundle/brown-noise.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Colored.bundle/green-noise.mp3
Normal file
BIN
TheNoiseClock/Resources/Colored.bundle/green-noise.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Colored.bundle/grey-noise.mp3
Normal file
BIN
TheNoiseClock/Resources/Colored.bundle/grey-noise.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Colored.bundle/pink-noise.mp3
Normal file
BIN
TheNoiseClock/Resources/Colored.bundle/pink-noise.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
TheNoiseClock/Resources/Mechanical.bundle/clock-ticking.mp3
Normal file
BIN
TheNoiseClock/Resources/Mechanical.bundle/clock-ticking.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Mechanical.bundle/electric-fan.mp3
Normal file
BIN
TheNoiseClock/Resources/Mechanical.bundle/electric-fan.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Mechanical.bundle/engine-idling.mp3
Normal file
BIN
TheNoiseClock/Resources/Mechanical.bundle/engine-idling.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Nature.bundle/crickets-night.mp3
Normal file
BIN
TheNoiseClock/Resources/Nature.bundle/crickets-night.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Nature.bundle/distant-thunderstorm.mp3
Normal file
BIN
TheNoiseClock/Resources/Nature.bundle/distant-thunderstorm.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Nature.bundle/forest-ambience.mp3
Normal file
BIN
TheNoiseClock/Resources/Nature.bundle/forest-ambience.mp3
Normal file
Binary file not shown.
BIN
TheNoiseClock/Resources/Nature.bundle/ocean-waves.mp3
Normal file
BIN
TheNoiseClock/Resources/Nature.bundle/ocean-waves.mp3
Normal file
Binary file not shown.
@ -42,14 +42,6 @@
|
||||
"bundleName": null
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "alarm",
|
||||
"name": "Alarm Sounds",
|
||||
"description": "Wake-up and notification alarm sounds",
|
||||
"bundleName": null
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"defaultVolume": 1.0,
|
||||
"defaultLoopCount": -1,
|
||||
|
||||
@ -4,45 +4,164 @@
|
||||
"id": "white-noise",
|
||||
"name": "White Noise",
|
||||
"fileName": "white-noise.mp3",
|
||||
"category": "ambient",
|
||||
"description": "Classic white noise for focus and relaxation",
|
||||
"bundleName": "Ambient"
|
||||
"category": "colored",
|
||||
"description": "Classic white noise with equal energy across frequencies for focus and relaxation",
|
||||
"bundleName": "Colored"
|
||||
},
|
||||
{
|
||||
"id": "pink-noise",
|
||||
"name": "Pink Noise",
|
||||
"fileName": "pink-noise.mp3",
|
||||
"category": "colored",
|
||||
"description": "Soft, warm noise resembling steady rain, ideal for relaxation",
|
||||
"bundleName": "Colored",
|
||||
"sourceUrl": "https://freesound.org/search/?q=pink+noise+loop"
|
||||
},
|
||||
{
|
||||
"id": "brown-noise",
|
||||
"name": "Brown Noise",
|
||||
"fileName": "brown-noise.mp3",
|
||||
"category": "colored",
|
||||
"description": "Deep, rumbling noise like distant thunder, great for deep sleep",
|
||||
"bundleName": "Colored"
|
||||
},
|
||||
{
|
||||
"id": "green-noise",
|
||||
"name": "Green Noise",
|
||||
"fileName": "green-noise.mp3",
|
||||
"category": "colored",
|
||||
"description": "Mid-range noise resembling rustling leaves, soothing and nature-like",
|
||||
"bundleName": "Colored"
|
||||
},
|
||||
{
|
||||
"id": "grey-noise",
|
||||
"name": "Grey Noise",
|
||||
"fileName": "grey-noise.mp3",
|
||||
"category": "colored",
|
||||
"description": "Balanced noise adjusted for human hearing, perfect for calm focus",
|
||||
"bundleName": "Colored"
|
||||
},
|
||||
{
|
||||
"id": "heavy-rain",
|
||||
"name": "Heavy Rain White Noise",
|
||||
"fileName": "heavy-rain-white-noise.mp3",
|
||||
"name": "Heavy Rain",
|
||||
"fileName": "heavy-rain.mp3",
|
||||
"category": "nature",
|
||||
"description": "Heavy rainfall sounds for peaceful sleep",
|
||||
"bundleName": "Nature"
|
||||
},
|
||||
{
|
||||
"id": "fan-noise",
|
||||
"name": "Fan White Noise",
|
||||
"fileName": "fan-white-noise-heater.mp3",
|
||||
"category": "mechanical",
|
||||
"description": "Fan and heater sounds for consistent background noise",
|
||||
"bundleName": "Mechanical"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "ambient",
|
||||
"name": "Ambient",
|
||||
"description": "General ambient sounds",
|
||||
"bundleName": "Ambient"
|
||||
},
|
||||
{
|
||||
"id": "nature",
|
||||
"name": "Nature",
|
||||
"description": "Natural environmental sounds",
|
||||
"id": "ocean-waves",
|
||||
"name": "Ocean Waves",
|
||||
"fileName": "ocean-waves.mp3",
|
||||
"category": "nature",
|
||||
"description": "Gentle waves crashing on the shore for relaxation",
|
||||
"bundleName": "Nature"
|
||||
},
|
||||
{
|
||||
"id": "mechanical",
|
||||
"name": "Mechanical",
|
||||
"description": "Mechanical and electronic sounds",
|
||||
"id": "forest-ambience",
|
||||
"name": "Forest Ambience",
|
||||
"fileName": "forest-ambience.mp3",
|
||||
"category": "nature",
|
||||
"description": "Calm forest sounds with birds and wind for nature lovers",
|
||||
"bundleName": "Nature"
|
||||
},
|
||||
{
|
||||
"id": "fan-noise",
|
||||
"name": "Fan Heater",
|
||||
"fileName": "fan-heater.mp3",
|
||||
"category": "mechanical",
|
||||
"description": "Fan and heater sounds for consistent background noise",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "air-conditioner",
|
||||
"name": "Air Conditioner Hum",
|
||||
"fileName": "air-conditioner-hum.mp3",
|
||||
"category": "mechanical",
|
||||
"description": "Steady hum of an air conditioner for focus or sleep",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "ambient-pad",
|
||||
"name": "Atmospheric Pad",
|
||||
"fileName": "atmospheric-pad.mp3",
|
||||
"category": "ambient",
|
||||
"description": "Soothing atmospheric drone for meditation or focus",
|
||||
"bundleName": "Ambient",
|
||||
"sourceUrl": "https://pixabay.com/sound-effects/search/ambient%20drone/"
|
||||
},
|
||||
{
|
||||
"id": "calm-pad",
|
||||
"name": "Calm Ambient Pad",
|
||||
"fileName": "calm-ambient-pad.mp3",
|
||||
"category": "ambient",
|
||||
"description": "Soft, warm ambient pad for deep relaxation",
|
||||
"bundleName": "Ambient",
|
||||
"sourceUrl": "https://pixabay.com/sound-effects/search/ambient%20pad/"
|
||||
},
|
||||
{
|
||||
"id": "dark-ambient",
|
||||
"name": "Dark Ambient Atmosphere",
|
||||
"fileName": "dark-ambient.mp3",
|
||||
"category": "ambient",
|
||||
"description": "A moody, atmospheric soundscape with deep tones, ideal for introspection or creative focus.",
|
||||
"bundleName": "Ambient"
|
||||
},
|
||||
{
|
||||
"id": "ethereal-ambient",
|
||||
"name": "Ethereal Ambient Soundscape",
|
||||
"fileName": "ethereal-ambient.mp3",
|
||||
"category": "ambient",
|
||||
"description": "A dreamy, ethereal sound with delicate tones, perfect for relaxation or spiritual practices.",
|
||||
"bundleName": "Ambient"
|
||||
},
|
||||
{
|
||||
"id": "ambient-waves",
|
||||
"name": "Ambient Waves",
|
||||
"fileName": "ambient-waves.mp3",
|
||||
"category": "ambient",
|
||||
"description": "A smooth, wave-like ambient sound, evoking a sense of calm flow. Ideal for meditation or focus.",
|
||||
"bundleName": "Ambient"
|
||||
},
|
||||
{
|
||||
"id": "clock-ticking",
|
||||
"name": "Clock Ticking Mechanism",
|
||||
"fileName": "clock-ticking.mp3",
|
||||
"category": "mechanical",
|
||||
"description": "The rhythmic ticking of a clock, providing a consistent, hypnotic sound for focus or relaxation.",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "electric-fan",
|
||||
"name": "Electric Fan Whirring",
|
||||
"fileName": "electric-fan.mp3",
|
||||
"category": "mechanical",
|
||||
"description": "The steady whir of an electric fan, creating a monotonous sound for sleep or concentration.",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "engine-idling",
|
||||
"name": "Engine Idling",
|
||||
"fileName": "engine-idling.mp3",
|
||||
"category": "mechanical",
|
||||
"description": "A low, steady engine idle, offering a deep hum for background noise or relaxation.",
|
||||
"bundleName": "Mechanical"
|
||||
},
|
||||
{
|
||||
"id": "crickets-night",
|
||||
"name": "Crickets at Night",
|
||||
"fileName": "crickets-night.mp3",
|
||||
"category": "nature",
|
||||
"description": "The rhythmic chirping of crickets under a night sky, perfect for calming sleep.",
|
||||
"bundleName": "Nature"
|
||||
},
|
||||
{
|
||||
"id": "distant-thunderstorm",
|
||||
"name": "Distant Thunderstorm",
|
||||
"fileName": "distant-thunderstorm.mp3",
|
||||
"category": "nature",
|
||||
"description": "Soft thunder and rain in the distance, creating a soothing, stormy ambiance.",
|
||||
"bundleName": "Nature"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@ -56,8 +56,8 @@ class AlarmSoundService {
|
||||
}
|
||||
|
||||
/// Get alarm sound categories
|
||||
func getAlarmSoundCategories() -> [SoundCategory] {
|
||||
return loadAlarmConfiguration().categories
|
||||
func getAlarmSoundCategories() -> [TheNoiseClock.SoundCategory] {
|
||||
return [.alarm]
|
||||
}
|
||||
|
||||
/// Get alarm sound settings
|
||||
|
||||
@ -14,56 +14,56 @@ struct SoundCategoryView: View {
|
||||
// MARK: - Properties
|
||||
let sounds: [Sound]
|
||||
@Binding var selectedSound: Sound?
|
||||
@State private var selectedCategory: String = "all"
|
||||
@State private var selectedCategory: SoundCategory = .all
|
||||
@State private var searchText: String = ""
|
||||
@State private var viewModel = SoundViewModel()
|
||||
|
||||
// MARK: - Computed Properties
|
||||
private var filteredSounds: [Sound] {
|
||||
let nonAlarmSounds = sounds.filter { $0.category != "alarm" }
|
||||
let nonAlarmSounds = sounds.filter { $0.category != SoundCategory.alarm.rawValue }
|
||||
|
||||
let categoryFiltered = selectedCategory == "all"
|
||||
let categoryFiltered = selectedCategory == .all
|
||||
? nonAlarmSounds
|
||||
: nonAlarmSounds.filter { $0.category == selectedCategory }
|
||||
: nonAlarmSounds.filter { $0.category == selectedCategory.rawValue }
|
||||
|
||||
if searchText.isEmpty {
|
||||
return categoryFiltered
|
||||
let searchFiltered = if searchText.isEmpty {
|
||||
categoryFiltered
|
||||
} else {
|
||||
return categoryFiltered.filter { sound in
|
||||
categoryFiltered.filter { sound in
|
||||
sound.name.localizedCaseInsensitiveContains(searchText) ||
|
||||
sound.description.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort sounds alphabetically by name
|
||||
return searchFiltered.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
private func getCategoryCount(for category: String) -> Int {
|
||||
let nonAlarmSounds = sounds.filter { $0.category != "alarm" }
|
||||
private func getCategoryCount(for category: SoundCategory) -> Int {
|
||||
let nonAlarmSounds = sounds.filter { $0.category != SoundCategory.alarm.rawValue }
|
||||
|
||||
if category == "all" {
|
||||
if category == .all {
|
||||
return nonAlarmSounds.count
|
||||
} else {
|
||||
return nonAlarmSounds.filter { $0.category == category }.count
|
||||
return nonAlarmSounds.filter { $0.category == category.rawValue }.count
|
||||
}
|
||||
}
|
||||
|
||||
private var categories: [String] {
|
||||
let nonAlarmSounds = sounds.filter { $0.category != "alarm" }
|
||||
let uniqueCategories = Set(nonAlarmSounds.map { $0.category })
|
||||
return ["all"] + Array(uniqueCategories).sorted()
|
||||
private var categories: [SoundCategory] {
|
||||
let nonAlarmSounds = sounds.filter { $0.category != SoundCategory.alarm.rawValue }
|
||||
let uniqueCategoryStrings = Set(nonAlarmSounds.map { $0.category })
|
||||
|
||||
// Convert string categories to enum cases and filter out invalid ones
|
||||
let validCategories = uniqueCategoryStrings.compactMap { SoundCategory(from: $0) }
|
||||
|
||||
// Always include "All" and filter other categories based on available sounds
|
||||
let allCategories = [SoundCategory.all] + validCategories
|
||||
|
||||
// Return sorted categories using the enum's sort order
|
||||
return SoundCategory.sortedNoiseCategories.filter { allCategories.contains($0) }
|
||||
}
|
||||
|
||||
private var categoryDisplayName: (String) -> String {
|
||||
return { category in
|
||||
switch category {
|
||||
case "all": return "All"
|
||||
case "ambient": return "Ambient"
|
||||
case "nature": return "Nature"
|
||||
case "mechanical": return "Mechanical"
|
||||
default: return category.capitalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
@ -103,9 +103,9 @@ struct SoundCategoryView: View {
|
||||
private var categoryTabs: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: UIConstants.Spacing.small) {
|
||||
ForEach(categories, id: \.self) { category in
|
||||
ForEach(categories) { category in
|
||||
CategoryTab(
|
||||
title: categoryDisplayName(category),
|
||||
title: category.displayName,
|
||||
isSelected: selectedCategory == category,
|
||||
count: getCategoryCount(for: category)
|
||||
) {
|
||||
@ -252,16 +252,7 @@ struct SoundCard: View {
|
||||
}
|
||||
|
||||
private var soundIcon: String {
|
||||
switch sound.category {
|
||||
case "ambient":
|
||||
return "waveform"
|
||||
case "nature":
|
||||
return "cloud.rain"
|
||||
case "mechanical":
|
||||
return "fan"
|
||||
default:
|
||||
return "speaker.wave.2"
|
||||
}
|
||||
return SoundCategory(from: sound.category)?.icon ?? "speaker.wave.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user