diff --git a/.resources/Recordings/AudioPlaybackWidget.gif b/.resources/Recordings/AudioPlaybackWidget.gif new file mode 100644 index 0000000..1f46e7c Binary files /dev/null and b/.resources/Recordings/AudioPlaybackWidget.gif differ diff --git a/.resources/Screenshots/AudioPlaybackWidget.png b/.resources/Screenshots/AudioPlaybackWidget.png new file mode 100644 index 0000000..f4bf5d7 Binary files /dev/null and b/.resources/Screenshots/AudioPlaybackWidget.png differ diff --git a/App/App.swift b/App/App.swift index 14da16f..07d6e69 100644 --- a/App/App.swift +++ b/App/App.swift @@ -50,5 +50,6 @@ extension WidgetExamplesApp { renderer.scale = 10 let filename = URL.documentsDirectory.appending(path: "SharedViewWidget.png") try? renderer.uiImage?.pngData()?.write(to: filename) + print(filename) } } diff --git a/App/Navigation/AppScreen.swift b/App/Navigation/AppScreen.swift index ce17b10..def2f31 100644 --- a/App/Navigation/AppScreen.swift +++ b/App/Navigation/AppScreen.swift @@ -24,6 +24,7 @@ import SwiftUI enum AppScreen: Hashable, CaseIterable { case appGroup + case audioPlayback case coreData case dynamicIntent case liveActivity @@ -44,6 +45,8 @@ extension AppScreen { switch self { case .appGroup: "App Group" + case .audioPlayback: + "Audio Playback" case .coreData: "Core Data" case .dynamicIntent: @@ -70,6 +73,8 @@ extension AppScreen { switch self { case .appGroup: AppGroupWidgetView() + case .audioPlayback: + AudioPlaybackWidgetView() case .coreData: CoreDataWidgetView() case .dynamicIntent: diff --git a/App/Screens/AudioPlayback/AudioPlaybackWidgetView.swift b/App/Screens/AudioPlayback/AudioPlaybackWidgetView.swift new file mode 100644 index 0000000..3d6f8b4 --- /dev/null +++ b/App/Screens/AudioPlayback/AudioPlaybackWidgetView.swift @@ -0,0 +1,80 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI +import WidgetKit + +struct AudioPlaybackWidgetView: View { + @AppStorage(UserDefaultKey.isAudioPlaying, store: .appGroup) + private var isPlaying = false + + var body: some View { + List { + Section { + contentView + } header: { + headerView + } + } + .onChange(of: isPlaying) { + reloadWidgetTimelines() + } + } +} + +// MARK: - Content + +extension AudioPlaybackWidgetView { + private var headerView: some View { + Text("Audio Playback") + } + + @ViewBuilder + private var contentView: some View { + stateView + buttonsView + } + + private var stateView: some View { + Text(isPlaying ? "Playing..." : "Paused") + } + + private var buttonsView: some View { + AudioPlaybackWidgetButtonsView(isPlaying: isPlaying) + } +} + +// MARK: - Helpers + +extension AudioPlaybackWidgetView { + private func reloadWidgetTimelines() { + WidgetCenter.shared.reloadTimelines(ofKind: WidgetType.audioPlayback.kind) + } +} + +// MARK: - Preview + +#Preview { + NavigationStack { + AudioPlaybackWidgetView() + } +} diff --git a/README.md b/README.md index 5e24118..1af0b24 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,10 @@ Widget Examples logo

- - Build - - - Language - - - Release version - - - License - + Build + Language + Release version + License

# Widget Examples @@ -25,14 +17,15 @@ A demo project showing different types of Widgets created with SwiftUI and Widge Table of Contents - 1. [Gallery](#gallery) - 2. [Unofficial gallery](#unofficial) + 1. [Basic widgets](#basic) + 2. [Intent widgets](#intent) + 2. [Unofficial widgets](#unofficial) 3. [Installation](#installation) 4. [License](#license) -## Gallery +## Basic widgets @@ -75,15 +68,12 @@ A demo project showing different types of Widgets created with SwiftUI and Widge - - - - - + - + - + - + + + + + + + + + + + + +
Digital Clock - Dynamic Intent + + Live Activity Environment - Intent -
@@ -91,9 +81,9 @@ A demo project showing different types of Widgets created with SwiftUI and Widge Digital Clock Widget - - Dynamic Intent Widget + + + Live Activity Widget @@ -101,81 +91,109 @@ A demo project showing different types of Widgets created with SwiftUI and Widge Environment Widget - - Intent Widget - -
- Interactive + Lock Screen - Live Activity + + Network - Lock Screen + Shared View + + SwiftData
- - Interactive Widget + + Lock Screen Widget - - Live Activity Widget + + + Network Widget - - Lock Screen Widget + + Shared View Widget + + + + SwiftData Widget
+ URL Image +
+ + URL Image Widget + +
+ +## Intent widgets + +The following widgets use Intents. Please refer to the [documentation](https://developer.apple.com/documentation/appintents/appintent) for a more detailed explanation. + + +
- Network + Audio Playback - Shared View + Dynamic Intent - SwiftData + Intent - URL Image + Interactive
- - Network Widget + + Audio Playback Widget - - Shared View Widget + + Dynamic Intent Widget - - SwiftData Widget + + Intent Widget - - URL Image Widget + + Interactive Widget
-## Unofficial gallery +## Unofficial widgets -Please be aware that the folowing widgets use private API. This means they not necessarily have to pass Apple Review and you use them at your own risk. +The folowing widgets use private API. Please bear in mind that they don't necessarily have to pass the Apple review process and you use them at your own risk. diff --git a/Shared/Components/AudioPlayer.swift b/Shared/Components/AudioPlayer.swift new file mode 100644 index 0000000..a79e73d --- /dev/null +++ b/Shared/Components/AudioPlayer.swift @@ -0,0 +1,103 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import AVFoundation +import OSLog + +final class AudioPlayer { + private enum State { + case playing + case paused + case stopped + case disabled + } + + static let shared = AudioPlayer() + + private var logger: Logger = .audioPlayer + private var player: AVAudioPlayer? + private var state: State + + private init() { + do { + try AVAudioSession.sharedInstance().setCategory(.playback) + try AVAudioSession.sharedInstance().setActive(true) + state = .stopped + } catch { + state = .disabled + logger.error("AVAudioSession init error: \(error)") + } + } + + var isEnabled: Bool { + state != .disabled + } + + var isPlaying: Bool { + state == .playing + } +} + +// MARK: - Actions + +extension AudioPlayer { + func play(sound: Sound) { + guard isEnabled else { + return + } + player = player ?? createPlayer(for: sound) + if let player { + state = .playing + player.play() + } + } + + func pause() { + guard isEnabled, let player else { + return + } + state = .paused + player.pause() + } + + func stop() { + guard isEnabled else { + return + } + state = .stopped + player?.stop() + player = nil + } +} + +// MARK: - Helpers + +extension AudioPlayer { + private func createPlayer(for sound: Sound) -> AVAudioPlayer? { + do { + return try AVAudioPlayer(contentsOf: sound.url) + } catch { + logger.error("Error creating player: \(error)") + return nil + } + } +} diff --git a/Shared/Components/UserDefaultKey.swift b/Shared/Components/UserDefaultKey.swift index 28ffb15..bb53a5f 100644 --- a/Shared/Components/UserDefaultKey.swift +++ b/Shared/Components/UserDefaultKey.swift @@ -26,6 +26,8 @@ enum UserDefaultKey { static var luckyNumber: String { #function } static var persons: String { #function } + static var isAudioPlaying: String { #function } + static func eventCounter(id: Int) -> String { "eventCounter-\(id)" } diff --git a/Shared/Logger.swift b/Shared/Logger.swift index b43fdbd..cde83d3 100644 --- a/Shared/Logger.swift +++ b/Shared/Logger.swift @@ -28,6 +28,7 @@ extension Logger { static let app = Logger(subsystem: subsystem, category: "App") static let widgets = Logger(subsystem: subsystem, category: "Widgets") + static let audioPlayer = Logger(subsystem: subsystem, category: "AudioPlayer") static let coreData = Logger(subsystem: subsystem, category: "CoreData") static let fileManager = Logger(subsystem: subsystem, category: "FileManager") } diff --git a/Shared/Models/AudioPlayback/Sound.swift b/Shared/Models/AudioPlayback/Sound.swift new file mode 100644 index 0000000..5751b4a --- /dev/null +++ b/Shared/Models/AudioPlayback/Sound.swift @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +enum Sound: String { + /// https://pixabay.com/music/beats-sad-soul-chasing-a-feeling-185750 + case main +} + +extension Sound { + var url: URL { + Bundle.main.url(forResource: rawValue, withExtension: "mp3")! + } +} diff --git a/Shared/WidgetType.swift b/Shared/WidgetType.swift index 34fdcd1..d416fd1 100644 --- a/Shared/WidgetType.swift +++ b/Shared/WidgetType.swift @@ -25,6 +25,7 @@ import Foundation enum WidgetType: String { case analogClock case appGroup + case audioPlayback case coreData case countdown case deepLink diff --git a/Widget Examples.xcodeproj/project.pbxproj b/Widget Examples.xcodeproj/project.pbxproj index 4748951..d6a67e1 100644 --- a/Widget Examples.xcodeproj/project.pbxproj +++ b/Widget Examples.xcodeproj/project.pbxproj @@ -22,6 +22,13 @@ E521D0542A63012000F6749D /* DynamicIntentWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E521D0532A63012000F6749D /* DynamicIntentWidget+EntryView.swift */; }; E521D0562A63012900F6749D /* DynamicIntentWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E521D0552A63012900F6749D /* DynamicIntentWidget+Provider.swift */; }; E521D0582A63013C00F6749D /* DynamicIntentWidget+Intent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E521D0572A63013C00F6749D /* DynamicIntentWidget+Intent.swift */; }; + E53E0F282BF0FEB900681CA8 /* AudioPlaybackWidget+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */; }; + E53E0F2B2BF104A300681CA8 /* AudioPlaybackWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53E0F2A2BF104A300681CA8 /* AudioPlaybackWidgetView.swift */; }; + E53E0F2E2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */; }; + E53E0F2F2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */; }; + E53E0F342BF10C8100681CA8 /* WidgetHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5757DB12A6AD4D700764349 /* WidgetHeaderView.swift */; }; + E53E0F372BF10CF600681CA8 /* AudioPlaybackWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31612BF0F27B00280186 /* AudioPlaybackWidget+EntryView.swift */; }; + E53E0F382BF10CF900681CA8 /* AudioPlaybackWidget+Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31602BF0F27B00280186 /* AudioPlaybackWidget+Entry.swift */; }; E542135A253879B700CCC9C3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5421359253879B700CCC9C3 /* App.swift */; }; E542135C253879B700CCC9C3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E542135B253879B700CCC9C3 /* ContentView.swift */; }; E54214122538AFEC00CCC9C3 /* SharedViewWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E542140A2538AF7800CCC9C3 /* SharedViewWidgetEntry.swift */; }; @@ -127,6 +134,16 @@ E58BB8F52A62E32100DA8303 /* IntentWidget+EntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58BB8F42A62E32100DA8303 /* IntentWidget+EntryView.swift */; }; E58BB8F72A62E32700DA8303 /* IntentWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58BB8F62A62E32700DA8303 /* IntentWidget+Provider.swift */; }; E58BB8F92A62E32C00DA8303 /* IntentWidget+Intent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58BB8F82A62E32C00DA8303 /* IntentWidget+Intent.swift */; }; + E5AE31662BF0F27B00280186 /* AudioPlaybackWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE315F2BF0F27B00280186 /* AudioPlaybackWidget.swift */; }; + E5AE31692BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */; }; + E5AE316A2BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31632BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift */; }; + E5AE316B2BF0F27B00280186 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = E5AE31642BF0F27B00280186 /* README.md */; }; + E5AE316E2BF0F4F700280186 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE316D2BF0F4F700280186 /* Sound.swift */; }; + E5AE316F2BF0F4F700280186 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE316D2BF0F4F700280186 /* Sound.swift */; }; + E5AE31712BF0F67200280186 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31702BF0F67200280186 /* AudioPlayer.swift */; }; + E5AE31722BF0F67200280186 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5AE31702BF0F67200280186 /* AudioPlayer.swift */; }; + E5AE31772BF0FCB400280186 /* main.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E5AE31762BF0FCB400280186 /* main.mp3 */; }; + E5AE31782BF0FCB400280186 /* main.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E5AE31762BF0FCB400280186 /* main.mp3 */; }; E5B345222A67E37D00702B86 /* LiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B345212A67E37D00702B86 /* LiveActivityWidget.swift */; }; E5B345242A67E3E400702B86 /* DeliveryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B345232A67E3E400702B86 /* DeliveryAttributes.swift */; }; E5B345252A67E3E400702B86 /* DeliveryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B345232A67E3E400702B86 /* DeliveryAttributes.swift */; }; @@ -208,6 +225,8 @@ E521D0532A63012000F6749D /* DynamicIntentWidget+EntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DynamicIntentWidget+EntryView.swift"; sourceTree = ""; }; E521D0552A63012900F6749D /* DynamicIntentWidget+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DynamicIntentWidget+Provider.swift"; sourceTree = ""; }; E521D0572A63013C00F6749D /* DynamicIntentWidget+Intent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DynamicIntentWidget+Intent.swift"; sourceTree = ""; }; + E53E0F2A2BF104A300681CA8 /* AudioPlaybackWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackWidgetView.swift; sourceTree = ""; }; + E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackWidgetButtonsView.swift; sourceTree = ""; }; E5421356253879B700CCC9C3 /* Widget Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Widget Examples.app"; sourceTree = BUILT_PRODUCTS_DIR; }; E5421359253879B700CCC9C3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; E542135B253879B700CCC9C3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -280,6 +299,15 @@ E58BB8F62A62E32700DA8303 /* IntentWidget+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentWidget+Provider.swift"; sourceTree = ""; }; E58BB8F82A62E32C00DA8303 /* IntentWidget+Intent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentWidget+Intent.swift"; sourceTree = ""; }; E591C5712551666A002044C8 /* IntentWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentWidget.swift; sourceTree = ""; }; + E5AE315F2BF0F27B00280186 /* AudioPlaybackWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlaybackWidget.swift; sourceTree = ""; }; + E5AE31602BF0F27B00280186 /* AudioPlaybackWidget+Entry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+Entry.swift"; sourceTree = ""; }; + E5AE31612BF0F27B00280186 /* AudioPlaybackWidget+EntryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+EntryView.swift"; sourceTree = ""; }; + E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+Intents.swift"; sourceTree = ""; }; + E5AE31632BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AudioPlaybackWidget+Provider.swift"; sourceTree = ""; }; + E5AE31642BF0F27B00280186 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + E5AE316D2BF0F4F700280186 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; }; + E5AE31702BF0F67200280186 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; + E5AE31762BF0FCB400280186 /* main.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = main.mp3; sourceTree = ""; }; E5B345212A67E37D00702B86 /* LiveActivityWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidget.swift; sourceTree = ""; }; E5B345232A67E3E400702B86 /* DeliveryAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryAttributes.swift; sourceTree = ""; }; E5B345262A67E56900702B86 /* Delivery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delivery.swift; sourceTree = ""; }; @@ -320,6 +348,7 @@ isa = PBXGroup; children = ( E58246CF2A66C646008180CC /* AppGroup */, + E53E0F292BF1049200681CA8 /* AudioPlayback */, E58246D02A66C64C008180CC /* CoreData */, E58246CC2A66A65B008180CC /* DynamicIntent */, E5B345342A67F88E00702B86 /* LiveActivity */, @@ -352,6 +381,23 @@ path = AnalogClockWidget; sourceTree = ""; }; + E53E0F292BF1049200681CA8 /* AudioPlayback */ = { + isa = PBXGroup; + children = ( + E53E0F2A2BF104A300681CA8 /* AudioPlaybackWidgetView.swift */, + ); + path = AudioPlayback; + sourceTree = ""; + }; + E53E0F2C2BF1069300681CA8 /* Shared */ = { + isa = PBXGroup; + children = ( + E5AE31622BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift */, + E53E0F2D2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift */, + ); + path = Shared; + sourceTree = ""; + }; E542134D253879B700CCC9C3 = { isa = PBXGroup; children = ( @@ -402,6 +448,7 @@ E542137425387A0300CCC9C3 /* WidgetBundle.swift */, E52FE12F2BD552CE00141AC0 /* AnalogClockWidget */, E54213C7253891CB00CCC9C3 /* AppGroupWidget */, + E5AE31652BF0F27B00280186 /* AudioPlaybackWidget */, E54214182538B57400CCC9C3 /* CoreDataWidget */, E542139A2538806F00CCC9C3 /* CountdownWidget */, E54213F42538A9A300CCC9C3 /* DeepLinkWidget */, @@ -550,6 +597,7 @@ E55498362551D1F40066870B /* Models */ = { isa = PBXGroup; children = ( + E5AE316C2BF0F4E900280186 /* AudioPlayback */, E56181CD2A69D856002E0CB2 /* CoreData */, E56181D02A69D8A3002E0CB2 /* DynamicIntent */, E56181CF2A69D87C002E0CB2 /* LiveActivity */, @@ -630,6 +678,7 @@ isa = PBXGroup; children = ( E5757DD32A6AF61F00764349 /* Assets.xcassets */, + E5AE31762BF0FCB400280186 /* main.mp3 */, ); path = Resources; sourceTree = ""; @@ -637,6 +686,7 @@ E58246742A6435B0008180CC /* Components */ = { isa = PBXGroup; children = ( + E5AE31702BF0F67200280186 /* AudioPlayer.swift */, E5619C562A6986D900F94132 /* DeepLink.swift */, E58246722A635A4B008180CC /* Loadable.swift */, E542141D2538B68200CCC9C3 /* PersistenceController.swift */, @@ -750,6 +800,27 @@ path = IntentWidget; sourceTree = ""; }; + E5AE31652BF0F27B00280186 /* AudioPlaybackWidget */ = { + isa = PBXGroup; + children = ( + E53E0F2C2BF1069300681CA8 /* Shared */, + E5AE315F2BF0F27B00280186 /* AudioPlaybackWidget.swift */, + E5AE31602BF0F27B00280186 /* AudioPlaybackWidget+Entry.swift */, + E5AE31612BF0F27B00280186 /* AudioPlaybackWidget+EntryView.swift */, + E5AE31632BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift */, + E5AE31642BF0F27B00280186 /* README.md */, + ); + path = AudioPlaybackWidget; + sourceTree = ""; + }; + E5AE316C2BF0F4E900280186 /* AudioPlayback */ = { + isa = PBXGroup; + children = ( + E5AE316D2BF0F4F700280186 /* Sound.swift */, + ); + path = AudioPlayback; + sourceTree = ""; + }; E5B345202A67E36A00702B86 /* LiveActivityWidget */ = { isa = PBXGroup; children = ( @@ -882,7 +953,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5AE316B2BF0F27B00280186 /* README.md in Resources */, E5757DD42A6AF61F00764349 /* Assets.xcassets in Resources */, + E5AE31782BF0FCB400280186 /* main.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -891,6 +964,7 @@ buildActionMask = 2147483647; files = ( E5757DD12A6AF2B500764349 /* Assets.xcassets in Resources */, + E5AE31772BF0FCB400280186 /* main.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -903,6 +977,7 @@ files = ( E5DCA1942A683E6000C59A50 /* LiveActivityView+Title.swift in Sources */, E58BB8D02A62D89500DA8303 /* EnvironmentWidget+Provider.swift in Sources */, + E5AE31722BF0F67200280186 /* AudioPlayer.swift in Sources */, E582467B2A643D8E008180CC /* NetworkWidget+Entry.swift in Sources */, E58BB8ED2A62D90C00DA8303 /* CountryWebRepository.swift in Sources */, E58BB8F72A62E32700DA8303 /* IntentWidget+Provider.swift in Sources */, @@ -926,14 +1001,18 @@ E57C3E112A674183002CF144 /* LockScreenWidget+Entry.swift in Sources */, E580A5D72A6E9A270007BA39 /* Logger.swift in Sources */, E58BB8F12A62D92700DA8303 /* PersistenceController.swift in Sources */, + E5AE31662BF0F27B00280186 /* AudioPlaybackWidget.swift in Sources */, E58BB8D32A62D89500DA8303 /* EnvironmentWidget+Entry.swift in Sources */, E542EEDA2BD565CD00964BEB /* AnalogClockWidget+Provider.swift in Sources */, E58BB8C62A62D88500DA8303 /* CountdownWidget+Provider.swift in Sources */, E58BB8BF2A62D86D00DA8303 /* AppGroupWidget+EntryView.swift in Sources */, E542EEDB2BD565CD00964BEB /* AnalogClockWidget.swift in Sources */, + E53E0F382BF10CF900681CA8 /* AudioPlaybackWidget+Entry.swift in Sources */, E5B345282A67E56900702B86 /* Delivery.swift in Sources */, + E53E0F372BF10CF600681CA8 /* AudioPlaybackWidget+EntryView.swift in Sources */, E5DCA18E2A683DFC00C59A50 /* LiveActivityView+Icon.swift in Sources */, E58246912A64560D008180CC /* InteractiveWidget+EntryView.swift in Sources */, + E5AE316A2BF0F27B00280186 /* AudioPlaybackWidget+Provider.swift in Sources */, E58BB8E92A62D8FB00DA8303 /* WidgetType.swift in Sources */, E58BB8C12A62D87200DA8303 /* CoreDataWidget+Entry.swift in Sources */, E5B345222A67E37D00702B86 /* LiveActivityWidget.swift in Sources */, @@ -958,6 +1037,7 @@ E58BB8E12A62D8B100DA8303 /* DynamicIntentWidget.swift in Sources */, E5DCA1972A683E8C00C59A50 /* LiveActivityView+Status.swift in Sources */, E58BB8CB2A62D88A00DA8303 /* DeepLinkWidget+Entry.swift in Sources */, + E5AE31692BF0F27B00280186 /* AudioPlaybackWidget+Intents.swift in Sources */, E58BB8CA2A62D88A00DA8303 /* DeepLinkWidget.swift in Sources */, E58246B52A65C943008180CC /* Product.swift in Sources */, E58BB8E72A62D8F500DA8303 /* Shared.swift in Sources */, @@ -973,6 +1053,7 @@ E58246BA2A65CBA3008180CC /* SwiftDataWidget+EntryView.swift in Sources */, E57C3DF02A66D33B002CF144 /* Document.swift in Sources */, E58246B82A65CB9C008180CC /* SwiftDataWidget+Entry.swift in Sources */, + E5AE316F2BF0F4F700280186 /* Sound.swift in Sources */, E58BB8BD2A62D86D00DA8303 /* AppGroupWidget+Provider.swift in Sources */, E5DCA19A2A6840BD00C59A50 /* DeliveryState.swift in Sources */, E58BB8BB2A62D86500DA8303 /* WidgetBundle.swift in Sources */, @@ -992,6 +1073,7 @@ E58BB8EE2A62D90C00DA8303 /* WebRepository.swift in Sources */, E58BB8F02A62D92400DA8303 /* DataModel.xcdatamodeld in Sources */, E58BB8DE2A62D8AA00DA8303 /* LockScreenWidget.swift in Sources */, + E53E0F2F2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */, E58BB8C42A62D88500DA8303 /* CountdownWidget+EntryView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1002,9 +1084,11 @@ files = ( E58246C42A6692FE008180CC /* AppScreen.swift in Sources */, E542135C253879B700CCC9C3 /* ContentView.swift in Sources */, + E5AE31712BF0F67200280186 /* AudioPlayer.swift in Sources */, E5638AEA265D859D00E73B05 /* WidgetType.swift in Sources */, E517B33A2A61B36F009072A3 /* SharedViewWidgetView.swift in Sources */, E5DCA1992A6840BD00C59A50 /* DeliveryState.swift in Sources */, + E53E0F342BF10C8100681CA8 /* WidgetHeaderView.swift in Sources */, E57C3DEF2A66D33B002CF144 /* Document.swift in Sources */, E5DCA18D2A683DFC00C59A50 /* LiveActivityView+Icon.swift in Sources */, E517B3342A61AF28009072A3 /* CoreDataWidgetView.swift in Sources */, @@ -1012,6 +1096,7 @@ E56D4CF02A69474E00742DFB /* FileManager+Extensions.swift in Sources */, E57C3DE92A66C8C2002CF144 /* Array+Extensions.swift in Sources */, E517B33C2A61E022009072A3 /* UserDefaultKey.swift in Sources */, + E53E0F282BF0FEB900681CA8 /* AudioPlaybackWidget+Intents.swift in Sources */, E517B3412A61E0C6009072A3 /* Shared.swift in Sources */, E58246CE2A66A66F008180CC /* DynamicIntentWidgetView+Person.swift in Sources */, E55498382551D2160066870B /* Person.swift in Sources */, @@ -1020,9 +1105,11 @@ E58246BF2A65CBBB008180CC /* SwiftDataWidgetView.swift in Sources */, E54214262538B77500CCC9C3 /* DataModel.xcdatamodeld in Sources */, E58246752A6435C1008180CC /* Loadable.swift in Sources */, + E53E0F2E2BF106AD00681CA8 /* AudioPlaybackWidgetButtonsView.swift in Sources */, E54214152538AFF000CCC9C3 /* SharedViewWidgetEntryView.swift in Sources */, E5DCA1962A683E8C00C59A50 /* LiveActivityView+Status.swift in Sources */, E517B3382A61B246009072A3 /* DynamicIntentWidgetView.swift in Sources */, + E5AE316E2BF0F4F700280186 /* Sound.swift in Sources */, E57C3DEC2A66CA11002CF144 /* UserDefaults+Extensions.swift in Sources */, E54214122538AFEC00CCC9C3 /* SharedViewWidgetEntry.swift in Sources */, E5B345382A67F9C900702B86 /* LiveActivityWidgetView+Model.swift in Sources */, @@ -1043,6 +1130,7 @@ E58246C62A669323008180CC /* AppSidebarList.swift in Sources */, E5B345242A67E3E400702B86 /* DeliveryAttributes.swift in Sources */, E58246B62A65CB8D008180CC /* Product.swift in Sources */, + E53E0F2B2BF104A300681CA8 /* AudioPlaybackWidgetView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Widget Examples.xcodeproj/xcshareddata/xcschemes/Widgets.xcscheme b/Widget Examples.xcodeproj/xcshareddata/xcschemes/Widgets.xcscheme new file mode 100644 index 0000000..8636aab --- /dev/null +++ b/Widget Examples.xcodeproj/xcshareddata/xcschemes/Widgets.xcscheme @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Entry.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Entry.swift new file mode 100644 index 0000000..0a683c3 --- /dev/null +++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Entry.swift @@ -0,0 +1,38 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import WidgetKit + +extension AudioPlaybackWidget { + struct Entry: TimelineEntry { + var date: Date = .now + var isPlaying: Bool + } +} + +// MARK: - Data + +extension AudioPlaybackWidget.Entry { + static var placeholder: Self { + .init(isPlaying: false) + } +} diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+EntryView.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+EntryView.swift new file mode 100644 index 0000000..59236e4 --- /dev/null +++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+EntryView.swift @@ -0,0 +1,38 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +extension AudioPlaybackWidget { + struct EntryView: View { + let entry: Entry + + var body: some View { + VStack(alignment: .leading) { + WidgetHeaderView(title: "Audio Playback") + Spacer() + AudioPlaybackWidgetButtonsView(isPlaying: entry.isPlaying) + } + .containerBackground(.clear, for: .widget) + } + } +} diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Provider.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Provider.swift new file mode 100644 index 0000000..dc6b915 --- /dev/null +++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget+Provider.swift @@ -0,0 +1,41 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import WidgetKit + +extension AudioPlaybackWidget { + struct Provider: TimelineProvider { + func placeholder(in context: Context) -> Entry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) { + completion(.placeholder) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let isPlaying = UserDefaults.appGroup.bool(forKey: UserDefaultKey.isAudioPlaying) + let entry = Entry(isPlaying: isPlaying) + completion(.init(entries: [entry], policy: .never)) + } + } +} diff --git a/Widgets/AudioPlaybackWidget/AudioPlaybackWidget.swift b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget.swift new file mode 100644 index 0000000..9cb6ea7 --- /dev/null +++ b/Widgets/AudioPlaybackWidget/AudioPlaybackWidget.swift @@ -0,0 +1,46 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI +import WidgetKit + +struct AudioPlaybackWidget: Widget { + private let kind: String = WidgetType.audioPlayback.kind + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { + EntryView(entry: $0) + } + .configurationDisplayName("Audio Playback Widget") + .description("Play music in the background directly from the Widget.") + .supportedFamilies([.systemSmall]) + } +} + +// MARK: - Preview + +#Preview(as: .systemSmall) { + AudioPlaybackWidget() +} timeline: { + AudioPlaybackWidget.Entry(isPlaying: false) + AudioPlaybackWidget.Entry(isPlaying: true) +} diff --git a/Widgets/AudioPlaybackWidget/README.md b/Widgets/AudioPlaybackWidget/README.md new file mode 100644 index 0000000..958bf8e --- /dev/null +++ b/Widgets/AudioPlaybackWidget/README.md @@ -0,0 +1,7 @@ +# Audio Playback Widget + +Play music in the background directly from the Widget. + +## Preview + +![Audio Playback Widget](../../.resources/Recordings/AudioPlaybackWidget.gif) diff --git a/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidget+Intents.swift b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidget+Intents.swift new file mode 100644 index 0000000..d34152e --- /dev/null +++ b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidget+Intents.swift @@ -0,0 +1,67 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import AppIntents +import AVFoundation +import WidgetKit + +// MARK: - PlayIntent + +struct AudioPlaybackWidgetPlayIntent: AudioPlaybackIntent { + static var title: LocalizedStringResource = "Play Music" + + private let sound: Sound + + init(sound: Sound) { + self.sound = sound + } + + init() { + self.init(sound: .main) + } + + func perform() async throws -> some IntentResult { + AudioPlayer.shared.play(sound: sound) + UserDefaults.appGroup.set( + AudioPlayer.shared.isPlaying, + forKey: UserDefaultKey.isAudioPlaying + ) + return .result() + } +} + +// MARK: - PauseIntent + +struct AudioPlaybackWidgetPauseIntent: AudioPlaybackIntent { + static var title: LocalizedStringResource = "Pause Music" + + init() {} + + func perform() async throws -> some IntentResult { + AudioPlayer.shared.pause() + UserDefaults.appGroup.set( + AudioPlayer.shared.isPlaying, + forKey: UserDefaultKey.isAudioPlaying + ) + return .result() + } +} diff --git a/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidgetButtonsView.swift b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidgetButtonsView.swift new file mode 100644 index 0000000..721342d --- /dev/null +++ b/Widgets/AudioPlaybackWidget/Shared/AudioPlaybackWidgetButtonsView.swift @@ -0,0 +1,55 @@ +// The MIT License (MIT) +// +// Copyright (c) 2024-Present Paweł Wiszenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +struct AudioPlaybackWidgetButtonsView: View { + let isPlaying: Bool + + var body: some View { + if isPlaying { + pauseButton + } else { + playButton + } + } +} + +// MARK: - Content + +extension AudioPlaybackWidgetButtonsView { + private var playButton: some View { + Button(intent: AudioPlaybackWidgetPlayIntent(sound: .main)) { + Image(systemName: "play") + .padding(2) + } + .buttonStyle(.bordered) + } + + private var pauseButton: some View { + Button(intent: AudioPlaybackWidgetPauseIntent()) { + Image(systemName: "pause") + .padding(2) + } + .buttonStyle(.bordered) + } +} diff --git a/Widgets/Resources/main.mp3 b/Widgets/Resources/main.mp3 new file mode 100644 index 0000000..99b6ce6 Binary files /dev/null and b/Widgets/Resources/main.mp3 differ diff --git a/Widgets/WidgetBundle.swift b/Widgets/WidgetBundle.swift index 7b2ec2d..6f3ccec 100644 --- a/Widgets/WidgetBundle.swift +++ b/Widgets/WidgetBundle.swift @@ -35,6 +35,7 @@ struct WidgetBundle1: WidgetBundle { var body: some Widget { AnalogClockWidget() AppGroupWidget() + AudioPlaybackWidget() CoreDataWidget() CountdownWidget() DeepLinkWidget()