-
Notifications
You must be signed in to change notification settings - Fork 2
Remember folder #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0ab603f
b3b4a8e
9dfee5c
622d360
60b65ab
215dbd9
28e57bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,7 @@ class Album: Identifiable, Equatable { | |
init (_ albumFullPath: String) { | ||
albumPath = albumFullPath | ||
let fileManager = FileManager.default | ||
let contents = try! fileManager.contentsOfDirectory(atPath: albumFullPath) | ||
let contents = try! fileManager.contentsOfDirectory(atPath: albumFullPath).sorted() | ||
for file in contents as [String] { | ||
var isDir: ObjCBool = false; | ||
let lcFile = file.lowercased() | ||
|
@@ -94,7 +94,7 @@ class Album: Identifiable, Equatable { | |
// Return an array of URLs, one for each music file in this album's folder. | ||
func getPlaylist () -> [URL] { | ||
let fileManager = FileManager.default | ||
let contents = try! fileManager.contentsOfDirectory(atPath: albumPath) | ||
let contents = try! fileManager.contentsOfDirectory(atPath: albumPath).sorted() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Order tracks by name (usually the track starts with a track number) |
||
var musicFileURLs: [URL] = [] | ||
for file in contents as [String] { | ||
let lcFile = file.lowercased() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,21 +21,25 @@ | |
"size" : "32x32" | ||
}, | ||
{ | ||
"filename" : "AppIcon128x128.png", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add some icons, so I know where the app is. |
||
"idiom" : "mac", | ||
"scale" : "1x", | ||
"size" : "128x128" | ||
}, | ||
{ | ||
"filename" : "[email protected]", | ||
"idiom" : "mac", | ||
"scale" : "2x", | ||
"size" : "128x128" | ||
}, | ||
{ | ||
"filename" : "AppIcon256x256.png", | ||
"idiom" : "mac", | ||
"scale" : "1x", | ||
"size" : "256x256" | ||
}, | ||
{ | ||
"filename" : "[email protected]", | ||
"idiom" : "mac", | ||
"scale" : "2x", | ||
"size" : "256x256" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,13 @@ | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>com.apple.security.app-sandbox</key> | ||
<true/> | ||
<key>com.apple.security.files.user-selected.read-only</key> | ||
<true/> | ||
<key>com.apple.security.app-sandbox</key> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some of this is needed in order to make the "remember folder" work (since there is no user action. |
||
<true/> | ||
<key>com.apple.security.assets.music.read-only</key> | ||
<true/> | ||
<key>com.apple.security.files.downloads.read-only</key> | ||
<true/> | ||
<key>com.apple.security.files.user-selected.read-only</key> | ||
<true/> | ||
</dict> | ||
</plist> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,13 @@ import SwiftUI | |
|
||
@main | ||
struct ColdwaveApp: App { | ||
|
||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate | ||
|
||
@StateObject var state: ColdwaveState = ColdwaveState() | ||
|
||
// Only available in macOS 13.0 and later | ||
//@Environment(\.openWindow) private var openWindow | ||
|
||
var body: some Scene { | ||
WindowGroup { | ||
ContentView(state: state) | ||
|
@@ -13,6 +17,11 @@ struct ColdwaveApp: App { | |
CommandGroup(before: CommandGroupPlacement.newItem) { | ||
Button(action: { openFolder() }, label: { Label("Open directory...", systemImage: "doc") }) | ||
.keyboardShortcut("o") | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to add a "modal" dialog to ask for a remote location (audio stream or remote file) |
||
/* | ||
Button(action: { openWindow(id: "open-location") }, label: { Label("Open location...", systemImage: "doc")}) | ||
.keyboardShortcut("l") | ||
*/ | ||
} | ||
CommandMenu("Utilities") { | ||
Button("Bigger") { | ||
|
@@ -23,8 +32,14 @@ struct ColdwaveApp: App { | |
}.keyboardShortcut("-") | ||
} | ||
} | ||
|
||
/* | ||
Window("Open location", id: "open-location") { | ||
LocationView() | ||
}.windowResizability(.contentSize) | ||
*/ | ||
} | ||
|
||
private func openFolder () { | ||
let dialog = NSOpenPanel(); | ||
dialog.title = "Choose root music directory | ABCD"; | ||
|
@@ -44,3 +59,27 @@ struct ColdwaveApp: App { | |
} | ||
|
||
} | ||
|
||
final class AppDelegate: NSObject, NSApplicationDelegate { | ||
func applicationDidFinishLaunching(_ notification: Notification) { | ||
let unwantedMenus = ["Edit","View" ] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove a couple of menu entries that don't do much for a music player |
||
|
||
let removeMenus = { | ||
unwantedMenus.forEach { | ||
guard let menu = NSApp.mainMenu?.item(withTitle: $0) else { return } | ||
NSApp.mainMenu?.removeItem(menu) | ||
} | ||
} | ||
|
||
NotificationCenter.default.addObserver( | ||
forName: NSMenu.didAddItemNotification, | ||
object: nil, | ||
queue: .main | ||
) { _ in | ||
// Must refresh after every time SwiftUI re adds | ||
removeMenus() | ||
} | ||
|
||
removeMenus() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,31 @@ | ||
import Foundation | ||
import AVFoundation | ||
import SwiftUI | ||
import UserNotifications | ||
import MediaPlayer | ||
|
||
class ColdwaveState: ObservableObject { | ||
|
||
|
||
@AppStorage("music.folder") var path = "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. retrieve last folder path from application store |
||
@Published var albums: [Album] = [] | ||
@Published var path: String = ""; | ||
//@Published var path = ""; | ||
@Published var currentAlbum: Album? | ||
@Published var currentTrack = 0 | ||
@Published var currentTitle: String = "" | ||
@Published var coverSize: CGFloat = DEFAULT_IMAGE_SIZE | ||
@Published var playlist: [URL] = [] | ||
@Published var amountPlayed: Double = 0.0 // in range 0...1 | ||
@Published var playing: Bool = false | ||
@Published var searchText: String = "" | ||
@Published var timePlayed = 0 | ||
@Published var timeRemaining = 0 | ||
|
||
let player: AVPlayer = AVPlayer() | ||
let npCenter: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default() | ||
|
||
init() { | ||
player.allowsExternalPlayback = true | ||
|
||
player.addPeriodicTimeObserver( | ||
forInterval: CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), | ||
queue: .main | ||
|
@@ -24,10 +34,23 @@ class ColdwaveState: ObservableObject { | |
// converting the track duration units to match the amountPlayed units. | ||
if let duration = self.player.currentItem?.duration.convertScale(t.timescale, method: CMTimeRoundingMethod.default) { | ||
self.amountPlayed = Double(t.value) / Double(duration.value) | ||
self.timePlayed = Int(t.seconds) | ||
let d = duration.seconds | ||
self.timeRemaining = d.isNaN ? 0 : Int(d - t.seconds) | ||
|
||
self.updateNpInfo() | ||
} | ||
} | ||
|
||
npCenter.playbackState = .stopped | ||
// setupRemoteControls() | ||
requestNotifications() | ||
|
||
if path != "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If app is starting up and there is a stored folder path, scan library |
||
albums = Album.scanLibrary(at: path) | ||
} | ||
} | ||
|
||
// It doesn't seem clean to put this (or the AVPlayer) on the state, but notification | ||
// targets have to be objc functions which have to be members of an NSObject or protocol. | ||
// I could probably factor the player field and these methods out into another class. | ||
|
@@ -47,6 +70,7 @@ class ColdwaveState: ObservableObject { | |
let track = AVPlayerItem(asset: AVAsset(url: playlist[trackNumber])) | ||
player.replaceCurrentItem(with: track) | ||
currentTrack = trackNumber; | ||
currentTitle = playlist[trackNumber].lastPathComponent | ||
// I seem to be getting double-starts on automatic transition to next track. | ||
// But removing the play() call causes it to stall on the transition. | ||
player.play() | ||
|
@@ -58,22 +82,130 @@ class ColdwaveState: ObservableObject { | |
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, | ||
object: track | ||
) | ||
|
||
npCenter.playbackState = .playing | ||
updateNpInfo() | ||
} else { | ||
player.pause() | ||
playing = false | ||
npCenter.playbackState = .stopped | ||
updateNpInfo() | ||
} | ||
} | ||
|
||
func pause () { | ||
player.pause() | ||
playing = false | ||
npCenter.playbackState = .stopped | ||
} | ||
|
||
func play () { | ||
if (player.currentItem != nil) { | ||
player.play() | ||
playing = true | ||
npCenter.playbackState = .playing | ||
} | ||
} | ||
|
||
func requestNotifications() { | ||
let center = UNUserNotificationCenter.current() | ||
center.requestAuthorization(options: [.alert, /* .sound, .badge,*/ .provisional]) { granted, error in | ||
if error != nil { | ||
NSLog("UN requestAuthoriziation %@", error!.localizedDescription) | ||
} else { | ||
NSLog("UN requestAuthorization %@", granted ? "granted" : "denied") | ||
} | ||
} | ||
} | ||
|
||
func notify(title : String, message : String) { | ||
let center = UNUserNotificationCenter.current() | ||
center.getNotificationSettings { settings in | ||
guard (settings.authorizationStatus == .authorized) || | ||
(settings.authorizationStatus == .provisional) else { | ||
NSLog("Notifications not authorizided nor provisional") | ||
return | ||
} | ||
|
||
if settings.alertSetting == .enabled { | ||
let content = UNMutableNotificationContent() | ||
content.title = title | ||
content.body = message | ||
content.categoryIdentifier = "abyrd.Coldwave" | ||
|
||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) | ||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) | ||
center.add(request) | ||
} else { | ||
NSLog("Alert notifications are disabled") | ||
} | ||
} | ||
} | ||
|
||
/* | ||
private func setupRemoteControls() { | ||
let commandCenter = MPRemoteCommandCenter.shared() | ||
|
||
commandCenter.pauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in | ||
NSLog("remote pause") | ||
notify(title: config!.title, message: "Pause") | ||
playerPause() | ||
return .success | ||
} | ||
commandCenter.playCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in | ||
NSLog("remote play") | ||
if current < 0 { | ||
playNext() | ||
} else { | ||
playerPlay() | ||
} | ||
return .success | ||
} | ||
commandCenter.togglePlayPauseCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in | ||
NSLog("remote playPause") | ||
if player.timeControlStatus == .playing { | ||
notify(title: config!.title, message: "Pause " + currentTitle()) | ||
playerPause() | ||
} else { | ||
if current < 0 { | ||
playNext() | ||
} else { | ||
playerPlay() | ||
} | ||
|
||
notify(title: config!.title, message: "Play " + currentTitle()) | ||
} | ||
return .success | ||
} | ||
commandCenter.nextTrackCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in | ||
NSLog("remote next") | ||
let index = selectNext(forward: true) | ||
playerSelect(index: index, play: true) | ||
notify(title: config!.title, message: "Play " + currentTitle()) | ||
return .success | ||
} | ||
commandCenter.previousTrackCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in | ||
NSLog("remote next") | ||
let index = selectNext(forward: false) | ||
playerSelect(index: index, play: true) | ||
notify(title: config!.title, message: "Play " + currentTitle()) | ||
return .success | ||
} | ||
} | ||
*/ | ||
|
||
private func updateNpInfo() { | ||
npCenter.nowPlayingInfo = [ | ||
MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue, | ||
MPNowPlayingInfoPropertyIsLiveStream: false, | ||
//MPNowPlayingInfoPropertyPlaybackRate: player.rate, | ||
MPMediaItemPropertyTitle: currentTitle, | ||
MPMediaItemPropertyPodcastTitle: currentTitle, | ||
MPNowPlayingInfoPropertyAssetURL: playlist[currentTrack], | ||
MPMediaItemPropertyArtist: currentAlbum?.artist ?? "", | ||
//MPMediaItemPropertyPlaybackDuration: player.currentItem?.duration as Any, | ||
//MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime(), | ||
//MPMediaItemPropertyArtwork: currentAlbum?.coverImagePath as Any, | ||
] | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Order folders by path