Merge from main
This commit is contained in:
+35
-11
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/backend/player"
|
"github.com/dweymouth/supersonic/backend/player"
|
||||||
"github.com/dweymouth/supersonic/backend/player/mpv"
|
"github.com/dweymouth/supersonic/backend/player/mpv"
|
||||||
"github.com/dweymouth/supersonic/backend/util"
|
"github.com/dweymouth/supersonic/backend/util"
|
||||||
|
"github.com/dweymouth/supersonic/backend/windows"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/20after4/configdir"
|
"github.com/20after4/configdir"
|
||||||
@@ -49,7 +50,7 @@ type App struct {
|
|||||||
LocalPlayer *mpv.Player
|
LocalPlayer *mpv.Player
|
||||||
UpdateChecker UpdateChecker
|
UpdateChecker UpdateChecker
|
||||||
MPRISHandler *MPRISHandler
|
MPRISHandler *MPRISHandler
|
||||||
WinSMTC *SMTC
|
WinSMTC *windows.SMTC
|
||||||
ipcServer ipc.IPCServer
|
ipcServer ipc.IPCServer
|
||||||
LrcLibFetcher *LrcLibFetcher
|
LrcLibFetcher *LrcLibFetcher
|
||||||
|
|
||||||
@@ -381,7 +382,7 @@ func (a *App) setupMPRIS(mprisAppName string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SetupWindowsSMTC(hwnd uintptr) {
|
func (a *App) SetupWindowsSMTC(hwnd uintptr) {
|
||||||
smtc, err := InitSMTCForWindow(hwnd)
|
smtc, err := windows.InitSMTCForWindow(hwnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error initializing SMTC: %d", err)
|
log.Printf("error initializing SMTC: %d", err)
|
||||||
return
|
return
|
||||||
@@ -389,17 +390,17 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) {
|
|||||||
a.WinSMTC = smtc
|
a.WinSMTC = smtc
|
||||||
smtc.UpdateMetadata(a.displayAppName, "")
|
smtc.UpdateMetadata(a.displayAppName, "")
|
||||||
|
|
||||||
smtc.OnButtonPressed(func(btn SMTCButton) {
|
smtc.OnButtonPressed(func(btn windows.SMTCButton) {
|
||||||
switch btn {
|
switch btn {
|
||||||
case SMTCButtonPlay:
|
case windows.SMTCButtonPlay:
|
||||||
a.PlaybackManager.Continue()
|
a.PlaybackManager.Continue()
|
||||||
case SMTCButtonPause:
|
case windows.SMTCButtonPause:
|
||||||
a.PlaybackManager.Pause()
|
a.PlaybackManager.Pause()
|
||||||
case SMTCButtonNext:
|
case windows.SMTCButtonNext:
|
||||||
a.PlaybackManager.SeekNext()
|
a.PlaybackManager.SeekNext()
|
||||||
case SMTCButtonPrevious:
|
case windows.SMTCButtonPrevious:
|
||||||
a.PlaybackManager.SeekBackOrPrevious()
|
a.PlaybackManager.SeekBackOrPrevious()
|
||||||
case SMTCButtonStop:
|
case windows.SMTCButtonStop:
|
||||||
a.PlaybackManager.Stop()
|
a.PlaybackManager.Stop()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -428,18 +429,37 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) {
|
|||||||
})
|
})
|
||||||
a.PlaybackManager.OnPlaying(func() {
|
a.PlaybackManager.OnPlaying(func() {
|
||||||
smtc.SetEnabled(true)
|
smtc.SetEnabled(true)
|
||||||
smtc.UpdatePlaybackState(SMTCPlaybackStatePlaying)
|
smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePlaying)
|
||||||
})
|
})
|
||||||
a.PlaybackManager.OnPaused(func() {
|
a.PlaybackManager.OnPaused(func() {
|
||||||
smtc.SetEnabled(true)
|
smtc.SetEnabled(true)
|
||||||
smtc.UpdatePlaybackState(SMTCPlaybackStatePaused)
|
smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePaused)
|
||||||
})
|
})
|
||||||
a.PlaybackManager.OnStopped(func() {
|
a.PlaybackManager.OnStopped(func() {
|
||||||
smtc.SetEnabled(false)
|
smtc.SetEnabled(false)
|
||||||
smtc.UpdatePlaybackState(SMTCPlaybackStateStopped)
|
smtc.UpdatePlaybackState(windows.SMTCPlaybackStateStopped)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SetupWindowsTaskbarButtons(hwnd uintptr) {
|
||||||
|
windows.InitializeTaskbarButtons(hwnd, func(btn windows.TaskbarButton) {
|
||||||
|
switch btn {
|
||||||
|
case windows.TaskbarButtonPrevious:
|
||||||
|
a.PlaybackManager.SeekBackOrPrevious()
|
||||||
|
case windows.TaskbarButtonPlayPause:
|
||||||
|
a.PlaybackManager.PlayPause()
|
||||||
|
case windows.TaskbarButtonNext:
|
||||||
|
a.PlaybackManager.SeekNext()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
a.PlaybackManager.OnPlaying(func() {
|
||||||
|
windows.SetTaskbarButtonIsPlaying(true)
|
||||||
|
})
|
||||||
|
f := func() { windows.SetTaskbarButtonIsPlaying(false) }
|
||||||
|
a.PlaybackManager.OnPaused(f)
|
||||||
|
a.PlaybackManager.OnStopped(f)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) LoginToDefaultServer() error {
|
func (a *App) LoginToDefaultServer() error {
|
||||||
serverCfg := a.ServerManager.GetDefaultServer()
|
serverCfg := a.ServerManager.GetDefaultServer()
|
||||||
if serverCfg == nil {
|
if serverCfg == nil {
|
||||||
@@ -551,6 +571,10 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
|
|||||||
return cli.SeekBackOrPrevious()
|
return cli.SeekBackOrPrevious()
|
||||||
case *FlagNext:
|
case *FlagNext:
|
||||||
return cli.SeekNext()
|
return cli.SeekNext()
|
||||||
|
case *FlagStop:
|
||||||
|
return cli.Stop()
|
||||||
|
case *FlagStopAfterCurrent:
|
||||||
|
return cli.StopAfterCurrent()
|
||||||
case *FlagShow:
|
case *FlagShow:
|
||||||
return cli.Show()
|
return cli.Show()
|
||||||
case VolumeCLIArg >= 0:
|
case VolumeCLIArg >= 0:
|
||||||
|
|||||||
+12
-10
@@ -22,16 +22,18 @@ var (
|
|||||||
SearchPlaylistCLIArg string = ""
|
SearchPlaylistCLIArg string = ""
|
||||||
SearchTrackCLIArg string = ""
|
SearchTrackCLIArg string = ""
|
||||||
|
|
||||||
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
|
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
|
||||||
FlagPause = flag.Bool("pause", false, "pause playback")
|
FlagPause = flag.Bool("pause", false, "pause playback")
|
||||||
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
|
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
|
||||||
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
|
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
|
||||||
FlagNext = flag.Bool("next", false, "seek to next track")
|
FlagNext = flag.Bool("next", false, "seek to next track")
|
||||||
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
|
FlagStop = flag.Bool("stop", false, "stop playback")
|
||||||
FlagShow = flag.Bool("show", false, "show minimized app")
|
FlagStopAfterCurrent = flag.Bool("stop-after-current", false, "stop playback after current track")
|
||||||
FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album or -play-playlist)")
|
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
|
||||||
FlagVersion = flag.Bool("version", false, "print app version and exit")
|
FlagShow = flag.Bool("show", false, "show minimized app")
|
||||||
FlagHelp = flag.Bool("help", false, "print command line options and exit")
|
FlagShuffle = flag.Bool("shuffle", false, "shuffle the tracklist (to be used with either -play-album or -play-playlist)")
|
||||||
|
FlagVersion = flag.Bool("version", false, "print app version and exit")
|
||||||
|
FlagHelp = flag.Bool("help", false, "print command line options and exit")
|
||||||
|
|
||||||
FlagPlayAlbum *bool
|
FlagPlayAlbum *bool
|
||||||
FlagPlayPlaylist *bool
|
FlagPlayPlaylist *bool
|
||||||
|
|||||||
+20
-19
@@ -7,25 +7,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PingPath = "/ping"
|
PingPath = "/ping"
|
||||||
PlayPath = "/transport/play"
|
PlayPath = "/transport/play"
|
||||||
PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle>
|
PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle>
|
||||||
PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle>
|
PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle>
|
||||||
PlayTrackPath = "/transport/play-track" // ?id=<track ID>
|
PlayTrackPath = "/transport/play-track" // ?id=<track ID>
|
||||||
SearchAlbumPath = "/transport/search-album" // ?s=<searchQuery>
|
SearchAlbumPath = "/transport/search-album" // ?s=<searchQuery>
|
||||||
SearchPlaylistPath = "/transport/search-playlist" // ?s=<searchQuery>
|
SearchPlaylistPath = "/transport/search-playlist" // ?s=<searchQuery>
|
||||||
SearchTrackPath = "/transport/search-track" // ?s=<searchQuery>
|
SearchTrackPath = "/transport/search-track" // ?s=<searchQuery>
|
||||||
PlayPausePath = "/transport/playpause"
|
PlayPausePath = "/transport/playpause"
|
||||||
PausePath = "/transport/pause"
|
PausePath = "/transport/pause"
|
||||||
StopPath = "/transport/stop"
|
StopPath = "/transport/stop"
|
||||||
PreviousPath = "/transport/previous"
|
StopAfterCurrentPath = "/transport/stop-after-current"
|
||||||
NextPath = "/transport/next"
|
PreviousPath = "/transport/previous"
|
||||||
TimePosPath = "/transport/timepos" // ?s=<seconds>
|
NextPath = "/transport/next"
|
||||||
SeekByPath = "/transport/seek-by" // ?s=<+/- seconds>
|
TimePosPath = "/transport/timepos" // ?s=<seconds>
|
||||||
VolumePath = "/volume" // ?v=<vol>
|
SeekByPath = "/transport/seek-by" // ?s=<+/- seconds>
|
||||||
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
|
VolumePath = "/volume" // ?v=<vol>
|
||||||
ShowPath = "/window/show"
|
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
|
||||||
QuitPath = "/window/quit"
|
ShowPath = "/window/show"
|
||||||
|
QuitPath = "/window/quit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ func (c *Client) PlayPause() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) Stop() error {
|
||||||
|
_, err := c.sendRequest(StopPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) StopAfterCurrent() error {
|
||||||
|
_, err := c.sendRequest(StopAfterCurrentPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) SeekNext() error {
|
func (c *Client) SeekNext() error {
|
||||||
_, err := c.sendRequest(NextPath)
|
_, err := c.sendRequest(NextPath)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type PlaybackHandler interface {
|
|||||||
Continue()
|
Continue()
|
||||||
SeekBackOrPrevious()
|
SeekBackOrPrevious()
|
||||||
SeekNext()
|
SeekNext()
|
||||||
|
SetStopAfterCurrent(bool)
|
||||||
SeekSeconds(float64)
|
SeekSeconds(float64)
|
||||||
SeekBySeconds(float64)
|
SeekBySeconds(float64)
|
||||||
Volume() int
|
Volume() int
|
||||||
@@ -75,6 +76,9 @@ func (s *serverImpl) createHandler() http.Handler {
|
|||||||
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
|
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
|
||||||
m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause))
|
m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause))
|
||||||
m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop))
|
m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop))
|
||||||
|
m.HandleFunc(StopAfterCurrentPath, s.makeSimpleEndpointHandler(func() {
|
||||||
|
s.pbHandler.SetStopAfterCurrent(true)
|
||||||
|
}))
|
||||||
m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious))
|
m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious))
|
||||||
m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
|
m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
|
||||||
m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds))
|
m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds))
|
||||||
|
|||||||
@@ -449,6 +449,16 @@ func toTrack(ch *jellyfin.Song) *mediaprovider.Track {
|
|||||||
t.FilePath = ch.MediaSources[0].Path
|
t.FilePath = ch.MediaSources[0].Path
|
||||||
t.Size = int64(ch.MediaSources[0].Size)
|
t.Size = int64(ch.MediaSources[0].Size)
|
||||||
t.BitRate = ch.MediaSources[0].Bitrate / 1000
|
t.BitRate = ch.MediaSources[0].Bitrate / 1000
|
||||||
|
if strs := ch.MediaSources[0].MediaStreams; len(strs) > 0 {
|
||||||
|
t.SampleRate = strs[0].SampleRate
|
||||||
|
t.BitDepth = strs[0].BitDepth
|
||||||
|
t.Channels = strs[0].Channels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ch.MediaStreams) > 0 {
|
||||||
|
t.SampleRate = max(t.SampleRate, ch.MediaStreams[0].SampleRate)
|
||||||
|
t.BitDepth = max(t.BitDepth, ch.MediaStreams[0].BitDepth)
|
||||||
|
t.Channels = max(t.Channels, ch.MediaStreams[0].Channels)
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ type Track struct {
|
|||||||
Comment string
|
Comment string
|
||||||
BPM int
|
BPM int
|
||||||
ReplayGain ReplayGainInfo
|
ReplayGain ReplayGainInfo
|
||||||
|
SampleRate int
|
||||||
|
BitDepth int
|
||||||
|
Channels int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplayGainInfo struct {
|
type ReplayGainInfo struct {
|
||||||
|
|||||||
@@ -573,6 +573,9 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
|
|||||||
Comment: ch.Comment,
|
Comment: ch.Comment,
|
||||||
BPM: ch.BPM,
|
BPM: ch.BPM,
|
||||||
ReplayGain: rGain,
|
ReplayGain: rGain,
|
||||||
|
SampleRate: ch.SamplingRate,
|
||||||
|
BitDepth: ch.BitDepth,
|
||||||
|
Channels: ch.ChannelCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ type playbackEngine struct {
|
|||||||
isRadio bool
|
isRadio bool
|
||||||
loopMode LoopMode
|
loopMode LoopMode
|
||||||
|
|
||||||
|
stopAfterCurrent bool // flag to stop playback after current track ends
|
||||||
|
|
||||||
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
|
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
|
||||||
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
|
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
|
||||||
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
|
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
|
||||||
@@ -334,6 +336,15 @@ func (p *playbackEngine) Stop() error {
|
|||||||
return p.player.Stop(false)
|
return p.player.Stop(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *playbackEngine) SetStopAfterCurrent(stopAfterCurrent bool) {
|
||||||
|
p.stopAfterCurrent = stopAfterCurrent
|
||||||
|
if p.stopAfterCurrent {
|
||||||
|
p.setNextTrack(-1) // clear next playing track from internal player, if any
|
||||||
|
} else if p.loopMode != LoopNone || p.nowPlayingIdx < len(p.playQueue)-1 {
|
||||||
|
p.needToSetNextTrack = true // need to restore next track to internal player queue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *playbackEngine) Pause() error {
|
func (p *playbackEngine) Pause() error {
|
||||||
return p.player.Pause()
|
return p.player.Pause()
|
||||||
}
|
}
|
||||||
@@ -634,6 +645,7 @@ func (p *playbackEngine) handleOnStopped() {
|
|||||||
p.alreadyScrobbled = false
|
p.alreadyScrobbled = false
|
||||||
p.wasStopped = true
|
p.wasStopped = true
|
||||||
p.nowPlayingIdx = -1
|
p.nowPlayingIdx = -1
|
||||||
|
p.stopAfterCurrent = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// to be invoked as soon as the next item in the queue that should play changes
|
// to be invoked as soon as the next item in the queue that should play changes
|
||||||
@@ -855,7 +867,7 @@ func (p *playbackEngine) handleTimePosUpdate(seeked bool) {
|
|||||||
meta = np.Metadata()
|
meta = np.Metadata()
|
||||||
}
|
}
|
||||||
isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > meta.Duration.Seconds()-10
|
isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > meta.Duration.Seconds()-10
|
||||||
if p.needToSetNextTrack && isNearEnd {
|
if p.needToSetNextTrack && !p.stopAfterCurrent && isNearEnd {
|
||||||
p.needToSetNextTrack = false
|
p.needToSetNextTrack = false
|
||||||
p.setNextTrack(p.nextPlayingIndex())
|
p.setNextTrack(p.nextPlayingIndex())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -669,6 +669,14 @@ func (p *PlaybackManager) PlayPause() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) SetStopAfterCurrent(stopAfterCurrent bool) {
|
||||||
|
p.engine.SetStopAfterCurrent(stopAfterCurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) IsStopAfterCurrent() bool {
|
||||||
|
return p.engine.stopAfterCurrent
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) enqueueAutoplayTracks() {
|
func (p *PlaybackManager) enqueueAutoplayTracks() {
|
||||||
nowPlaying := p.NowPlaying()
|
nowPlaying := p.NowPlaying()
|
||||||
if nowPlaying == nil {
|
if nowPlaying == nil {
|
||||||
|
|||||||
@@ -229,6 +229,9 @@ func (d *DLNAPlayer) SetNextFile(url string, meta mediaprovider.MediaItemMetadat
|
|||||||
Title: meta.Name,
|
Title: meta.Name,
|
||||||
Seekable: true,
|
Seekable: true,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// empty media item to signify erasing next track in device queue
|
||||||
|
media = &avtransport.MediaItem{}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package backend
|
package windows
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -I .
|
#cgo CFLAGS: -I .
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package backend
|
package windows
|
||||||
|
|
||||||
/*
|
/*
|
||||||
void btn_callback_cgo(int in) {
|
void btn_callback_cgo(int in) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package backend
|
package windows
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shobjidl.h>
|
||||||
|
#include <initguid.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "taskbar_buttons.h"
|
||||||
|
|
||||||
|
DEFINE_GUID(IID_ITaskbarList3,
|
||||||
|
0xEA1AFB91, 0x9E28, 0x4B86, 0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF);
|
||||||
|
|
||||||
|
#define WM_SET_PLAYING_STATE (WM_APP + 1)
|
||||||
|
|
||||||
|
static ITaskbarList3 *g_taskbar = NULL;
|
||||||
|
static ThumbnailCallback g_callback = NULL;
|
||||||
|
static HWND g_mainHWnd = NULL;
|
||||||
|
static WNDPROC g_originalProc = NULL;
|
||||||
|
|
||||||
|
THUMBBUTTON g_thumbButtons[3];
|
||||||
|
|
||||||
|
static HICON g_prevIcon;
|
||||||
|
static HICON g_nextIcon;
|
||||||
|
static HICON g_playIcon;
|
||||||
|
static HICON g_pauseIcon;
|
||||||
|
|
||||||
|
static wchar_t g_tooltipPlay[64] = {0};
|
||||||
|
static wchar_t g_tooltipPause[64] = {0};
|
||||||
|
static wchar_t g_tooltipPrev[64] = {0};
|
||||||
|
static wchar_t g_tooltipNext[64] = {0};
|
||||||
|
|
||||||
|
static void utf8_to_utf16(const char* utf8, wchar_t* utf16buf, size_t maxChars) {
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16buf, (int)maxChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_tooltips_utf8(const char* prev, const char* next, const char* play, const char* pause) {
|
||||||
|
utf8_to_utf16(play, g_tooltipPlay, _countof(g_tooltipPlay));
|
||||||
|
utf8_to_utf16(pause, g_tooltipPause, _countof(g_tooltipPause));
|
||||||
|
utf8_to_utf16(prev, g_tooltipPrev, _countof(g_tooltipPrev));
|
||||||
|
utf8_to_utf16(next, g_tooltipNext, _countof(g_tooltipNext));
|
||||||
|
}
|
||||||
|
|
||||||
|
static HICON create_icon_from_bgra(const void *bgra, int width, int height) {
|
||||||
|
HICON hIcon = NULL;
|
||||||
|
|
||||||
|
// Create mask bitmap (not used, but required)
|
||||||
|
HBITMAP hMonoMask = CreateBitmap(width, height, 1, 1, NULL);
|
||||||
|
|
||||||
|
// Create color bitmap from provided pixels
|
||||||
|
BITMAPV5HEADER bi;
|
||||||
|
ZeroMemory(&bi, sizeof(bi));
|
||||||
|
bi.bV5Size = sizeof(bi);
|
||||||
|
bi.bV5Width = width;
|
||||||
|
bi.bV5Height = -height; // top-down DIB
|
||||||
|
bi.bV5Planes = 1;
|
||||||
|
bi.bV5BitCount = 32;
|
||||||
|
bi.bV5Compression = BI_BITFIELDS;
|
||||||
|
bi.bV5RedMask = 0x00FF0000;
|
||||||
|
bi.bV5GreenMask = 0x0000FF00;
|
||||||
|
bi.bV5BlueMask = 0x000000FF;
|
||||||
|
bi.bV5AlphaMask = 0xFF000000;
|
||||||
|
|
||||||
|
void *bits = NULL;
|
||||||
|
HDC hdc = GetDC(NULL);
|
||||||
|
HBITMAP hBmp = CreateDIBSection(hdc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, &bits, NULL, 0);
|
||||||
|
ReleaseDC(NULL, hdc);
|
||||||
|
|
||||||
|
if (hBmp && bits) {
|
||||||
|
memcpy(bits, bgra, width * height * 4);
|
||||||
|
|
||||||
|
ICONINFO ii;
|
||||||
|
ZeroMemory(&ii, sizeof(ii));
|
||||||
|
ii.fIcon = TRUE;
|
||||||
|
ii.hbmMask = hMonoMask;
|
||||||
|
ii.hbmColor = hBmp;
|
||||||
|
|
||||||
|
hIcon = CreateIconIndirect(&ii);
|
||||||
|
|
||||||
|
DeleteObject(hBmp);
|
||||||
|
DeleteObject(hMonoMask);
|
||||||
|
}
|
||||||
|
return hIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
int initialize_taskbar_icons(const void *bgraPrev, const void *bgraNext, const void *bgraPlay, const void *bgraPause, int width, int height) {
|
||||||
|
g_prevIcon = create_icon_from_bgra(bgraPrev, width, height);
|
||||||
|
g_nextIcon = create_icon_from_bgra(bgraNext, width, height);
|
||||||
|
g_playIcon = create_icon_from_bgra(bgraPlay, width, height);
|
||||||
|
g_pauseIcon = create_icon_from_bgra(bgraPause, width, height);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK OverrideWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (msg == WM_COMMAND) {
|
||||||
|
int buttonId = LOWORD(wParam);
|
||||||
|
if (g_callback) {
|
||||||
|
g_callback(buttonId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg == WM_SET_PLAYING_STATE) {
|
||||||
|
if (g_taskbar && g_mainHWnd && g_playIcon && g_pauseIcon) {
|
||||||
|
int playing = LOWORD(wParam) ? 1 : 0;
|
||||||
|
g_thumbButtons[1].hIcon = playing ? g_pauseIcon : g_playIcon;
|
||||||
|
wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), playing ? g_tooltipPause : g_tooltipPlay);
|
||||||
|
g_taskbar->lpVtbl->ThumbBarUpdateButtons(
|
||||||
|
g_taskbar,
|
||||||
|
g_mainHWnd,
|
||||||
|
ARRAYSIZE(g_thumbButtons),
|
||||||
|
g_thumbButtons
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallWindowProc(g_originalProc, hwnd, msg, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
int set_is_playing(int playing) {
|
||||||
|
if (g_mainHWnd) {
|
||||||
|
PostMessage(g_mainHWnd, WM_SET_PLAYING_STATE, (WPARAM)playing, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) {
|
||||||
|
g_callback = cb;
|
||||||
|
g_mainHWnd = (HWND)hwndPtr;
|
||||||
|
|
||||||
|
// subclass the WndProc with our OverrideWndProc
|
||||||
|
g_originalProc = (WNDPROC)SetWindowLongPtr(g_mainHWnd, GWLP_WNDPROC, (LONG_PTR)OverrideWndProc);
|
||||||
|
|
||||||
|
CoInitialize(NULL);
|
||||||
|
CoCreateInstance(&CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, (void**)&g_taskbar);
|
||||||
|
|
||||||
|
if (g_taskbar) {
|
||||||
|
g_taskbar->lpVtbl->HrInit(g_taskbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_taskbar || !g_mainHWnd) return -1;
|
||||||
|
|
||||||
|
ZeroMemory(g_thumbButtons, sizeof(g_thumbButtons));
|
||||||
|
|
||||||
|
g_thumbButtons[0].dwMask = THB_FLAGS | THB_TOOLTIP;
|
||||||
|
g_thumbButtons[0].iId = 1;
|
||||||
|
g_thumbButtons[0].dwFlags = THBF_ENABLED;
|
||||||
|
wcscpy_s(g_thumbButtons[0].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), g_tooltipPrev);
|
||||||
|
if (g_prevIcon) {
|
||||||
|
g_thumbButtons[0].dwMask |= THB_ICON;
|
||||||
|
g_thumbButtons[0].hIcon = g_prevIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_thumbButtons[1].dwMask = THB_FLAGS | THB_TOOLTIP;
|
||||||
|
g_thumbButtons[1].iId = 2;
|
||||||
|
g_thumbButtons[1].dwFlags = THBF_ENABLED;
|
||||||
|
wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), g_tooltipPlay);
|
||||||
|
if (g_playIcon) {
|
||||||
|
g_thumbButtons[1].dwMask |= THB_ICON;
|
||||||
|
g_thumbButtons[1].hIcon = g_playIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_thumbButtons[2].dwMask = THB_FLAGS | THB_TOOLTIP;
|
||||||
|
g_thumbButtons[2].iId = 3;
|
||||||
|
g_thumbButtons[2].dwFlags = THBF_ENABLED;
|
||||||
|
wcscpy_s(g_thumbButtons[2].szTip, ARRAYSIZE(g_thumbButtons[2].szTip), g_tooltipNext);
|
||||||
|
if (g_nextIcon) {
|
||||||
|
g_thumbButtons[2].dwMask |= THB_ICON;
|
||||||
|
g_thumbButtons[2].hIcon = g_nextIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_taskbar->lpVtbl->ThumbBarAddButtons(g_taskbar, g_mainHWnd, ARRAYSIZE(g_thumbButtons), g_thumbButtons);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lole32
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "taskbar_buttons.h"
|
||||||
|
|
||||||
|
extern void goButtonClicked(int);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gTaskbarButtonCallback func(TaskbarButton)
|
||||||
|
|
||||||
|
// InitializeTaskbarIcons supplies the image icons that will be used for the taskbar buttons.
|
||||||
|
// They must all have the same pixel dimensions.
|
||||||
|
// This should be called before InitializeTaskbarButtons or the buttons will not have icons.
|
||||||
|
func InitializeTaskbarIcons(prev, next, play, pause image.Image) error {
|
||||||
|
pB := imageToBGRA(prev)
|
||||||
|
nB := imageToBGRA(next)
|
||||||
|
plB := imageToBGRA(play)
|
||||||
|
paB := imageToBGRA(pause)
|
||||||
|
bnds := prev.Bounds()
|
||||||
|
|
||||||
|
C.initialize_taskbar_icons(unsafe.Pointer(&pB[0]), unsafe.Pointer(&nB[0]), unsafe.Pointer(&plB[0]), unsafe.Pointer(&paB[0]), C.int(bnds.Dx()), C.int(bnds.Dy()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTaskbarButtonToolTips should be called before InitializeTaskbarButtons to
|
||||||
|
// set the tool tips that will be used for the buttons
|
||||||
|
func SetTaskbarButtonToolTips(prev, next, play, pause string) error {
|
||||||
|
cPlay := C.CString(play)
|
||||||
|
defer C.free(unsafe.Pointer(cPlay))
|
||||||
|
cPause := C.CString(pause)
|
||||||
|
defer C.free(unsafe.Pointer(cPause))
|
||||||
|
cPrev := C.CString(prev)
|
||||||
|
defer C.free(unsafe.Pointer(cPrev))
|
||||||
|
cNext := C.CString(next)
|
||||||
|
defer C.free(unsafe.Pointer(cNext))
|
||||||
|
C.set_tooltips_utf8(cPrev, cNext, cPlay, cPause)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error {
|
||||||
|
gTaskbarButtonCallback = callback
|
||||||
|
C.initialize_taskbar_buttons(unsafe.Pointer(hwnd), C.ThumbnailCallback(C.goButtonClicked))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTaskbarButtonIsPlaying(isPlaying bool) error {
|
||||||
|
i := C.int(0)
|
||||||
|
if isPlaying {
|
||||||
|
i = C.int(1)
|
||||||
|
}
|
||||||
|
if ret := int(C.set_is_playing(i)); ret == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("failed to set taskbar button is playing state")
|
||||||
|
}
|
||||||
|
|
||||||
|
//export goButtonClicked
|
||||||
|
func goButtonClicked(buttonID C.int) {
|
||||||
|
if gTaskbarButtonCallback != nil {
|
||||||
|
gTaskbarButtonCallback(TaskbarButton(buttonID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageToBGRA converts an image.Image to a BGRA byte slice.
|
||||||
|
// For *image.RGBA, it un-premultiplies the colors to straight alpha.
|
||||||
|
func imageToBGRA(img image.Image) []byte {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
w, h := bounds.Dx(), bounds.Dy()
|
||||||
|
bgra := make([]byte, w*h*4)
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
switch src := img.(type) {
|
||||||
|
case *image.NRGBA:
|
||||||
|
// Straight alpha, just swap channels
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
off := src.PixOffset(bounds.Min.X, y)
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
r := src.Pix[off+0]
|
||||||
|
g := src.Pix[off+1]
|
||||||
|
b := src.Pix[off+2]
|
||||||
|
a := src.Pix[off+3]
|
||||||
|
bgra[i+0] = b
|
||||||
|
bgra[i+1] = g
|
||||||
|
bgra[i+2] = r
|
||||||
|
bgra[i+3] = a
|
||||||
|
off += 4
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *image.RGBA:
|
||||||
|
// Premultiplied alpha, un-premultiply to straight alpha
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
off := src.PixOffset(bounds.Min.X, y)
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
r := src.Pix[off+0]
|
||||||
|
g := src.Pix[off+1]
|
||||||
|
b := src.Pix[off+2]
|
||||||
|
a := src.Pix[off+3]
|
||||||
|
|
||||||
|
if a != 0 {
|
||||||
|
// Convert from premultiplied to straight alpha
|
||||||
|
r = uint8((uint16(r) * 0xFF) / uint16(a))
|
||||||
|
g = uint8((uint16(g) * 0xFF) / uint16(a))
|
||||||
|
b = uint8((uint16(b) * 0xFF) / uint16(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
bgra[i+0] = b
|
||||||
|
bgra[i+1] = g
|
||||||
|
bgra[i+2] = r
|
||||||
|
bgra[i+3] = a
|
||||||
|
off += 4
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Fallback: handle any other image.Image type via At()
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
r16, g16, b16, a16 := img.At(x, y).RGBA()
|
||||||
|
// Convert from 16-bit [0, 65535] to 8-bit [0, 255]
|
||||||
|
r := uint8(r16 >> 8)
|
||||||
|
g := uint8(g16 >> 8)
|
||||||
|
b := uint8(b16 >> 8)
|
||||||
|
a := uint8(a16 >> 8)
|
||||||
|
bgra[i+0] = b
|
||||||
|
bgra[i+1] = g
|
||||||
|
bgra[i+2] = r
|
||||||
|
bgra[i+3] = a
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bgra
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef void (*ThumbnailCallback)(int buttonId);
|
||||||
|
|
||||||
|
// sets the tool tip strings for prev, next, play, and pause
|
||||||
|
// should be called before initialize_taskbar_buttons or they will not have tool tips
|
||||||
|
void set_tooltips_utf8(const char* prev, const char* next, const char* play, const char* pause);
|
||||||
|
|
||||||
|
// sets the icons that will be used for the buttons.
|
||||||
|
// the arguments are pointers to BGRA pixel data, and the dimensions (w, h) of all 4 images.
|
||||||
|
int initialize_taskbar_icons(const void *bgraPrev, const void *bgraNext, const void *bgraPlay, const void *bgraPause, int width, int height);
|
||||||
|
|
||||||
|
// adds the buttons to the window and registers the given callback to receive the button press events.
|
||||||
|
int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb);
|
||||||
|
|
||||||
|
// sets whether the player is playing. controls which icon/tooltip the center button uses.
|
||||||
|
// safe to call from any thread
|
||||||
|
int set_is_playing(int playing);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package windows
|
||||||
|
|
||||||
|
type TaskbarButton int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskbarButtonPrevious TaskbarButton = 1
|
||||||
|
TaskbarButtonPlayPause TaskbarButton = 2
|
||||||
|
TaskbarButtonNext TaskbarButton = 3
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
var err = errors.New("taskbar buttons unsupported")
|
||||||
|
|
||||||
|
func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTaskbarButtonToolTips(prev, next, play, pause string) error {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeTaskbarIcons(prev, next, play, pause image.Image) error {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTaskbarButtonIsPlaying(isPlaying bool) error {
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
||||||
github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c
|
github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c
|
||||||
github.com/dweymouth/fyne-tooltip v0.3.0
|
github.com/dweymouth/fyne-tooltip v0.3.0
|
||||||
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f
|
github.com/dweymouth/go-jellyfin v0.0.0-20250808023725-196437af15a6
|
||||||
github.com/go-audio/audio v1.0.0
|
github.com/go-audio/audio v1.0.0
|
||||||
github.com/go-audio/wav v1.1.0
|
github.com/go-audio/wav v1.1.0
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4 h1:Q3r94Ac
|
|||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250712002006-5064d705dac4/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
||||||
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f h1:QsKPwFpTHuYHEEuhvp4VBClkHh00bNNgQ/2Ij1bkk8M=
|
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f h1:QsKPwFpTHuYHEEuhvp4VBClkHh00bNNgQ/2Ij1bkk8M=
|
||||||
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
github.com/dweymouth/go-jellyfin v0.0.0-20250716005557-0ea2becece1f/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||||
|
github.com/dweymouth/go-jellyfin v0.0.0-20250808023725-196437af15a6 h1:IFewD1zMs7eWkqmzdGeQP9lavYboJHwGqm1oOa7wZC8=
|
||||||
|
github.com/dweymouth/go-jellyfin v0.0.0-20250808023725-196437af15a6/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||||
github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0 h1:mYcctuWgVArHhSLJxndlUM43C3hoE18BLDBkXKM2tl0=
|
github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0 h1:mYcctuWgVArHhSLJxndlUM43C3hoE18BLDBkXKM2tl0=
|
||||||
github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0/go.mod h1:bp2870jtp/ixAJLIOdShBfl1WpyLGDZ57jnVWMgkgIc=
|
github.com/dweymouth/go-wav v0.0.0-20250719173115-e60429a83eb0/go.mod h1:bp2870jtp/ixAJLIOdShBfl1WpyLGDZ57jnVWMgkgIc=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/png"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -10,7 +12,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend"
|
"github.com/dweymouth/supersonic/backend"
|
||||||
|
"github.com/dweymouth/supersonic/backend/windows"
|
||||||
"github.com/dweymouth/supersonic/res"
|
"github.com/dweymouth/supersonic/res"
|
||||||
|
"github.com/dweymouth/supersonic/res/wintaskbarthumbs"
|
||||||
"github.com/dweymouth/supersonic/ui"
|
"github.com/dweymouth/supersonic/ui"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
@@ -85,6 +89,20 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
if err := initWindowsTaskbarIcons(); err != nil {
|
||||||
|
log.Printf("Error initializing taskbar thumbnail icons: %s", err.Error())
|
||||||
|
}
|
||||||
|
if err := windows.SetTaskbarButtonToolTips(
|
||||||
|
lang.L("Previous"),
|
||||||
|
lang.L("Next"),
|
||||||
|
lang.L("Play"),
|
||||||
|
lang.L("Pause"),
|
||||||
|
); err != nil {
|
||||||
|
log.Printf("error initializing taskbar button tool tips: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fyneApp := app.New()
|
fyneApp := app.New()
|
||||||
fyneApp.SetIcon(res.ResAppicon256Png)
|
fyneApp.SetIcon(res.ResAppicon256Png)
|
||||||
|
|
||||||
@@ -101,13 +119,13 @@ func main() {
|
|||||||
mainWindow.Controller.DoConnectToServerWorkflow(defaultServer)
|
mainWindow.Controller.DoConnectToServerWorkflow(defaultServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if myApp.Config.Application.EnableOSMediaPlayerAPIs {
|
if runtime.GOOS == "windows" {
|
||||||
mainWindow.Window.(driver.NativeWindow).RunNative(func(ctx any) {
|
mainWindow.Window.(driver.NativeWindow).RunNative(func(ctx any) {
|
||||||
// intialize Windows SMTC
|
hwnd := ctx.(driver.WindowsWindowContext).HWND
|
||||||
if runtime.GOOS == "windows" {
|
if myApp.Config.Application.EnableOSMediaPlayerAPIs {
|
||||||
hwnd := ctx.(driver.WindowsWindowContext).HWND
|
|
||||||
myApp.SetupWindowsSMTC(hwnd)
|
myApp.SetupWindowsSMTC(hwnd)
|
||||||
}
|
}
|
||||||
|
myApp.SetupWindowsTaskbarButtons(hwnd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -126,3 +144,26 @@ func main() {
|
|||||||
log.Println("Running shutdown tasks...")
|
log.Println("Running shutdown tasks...")
|
||||||
myApp.Shutdown()
|
myApp.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initWindowsTaskbarIcons() error {
|
||||||
|
play, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaPlayPNG))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pause, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaPausePNG))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prev, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaSeekPreviousPNG))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaSeekNextPNG))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
windows.InitializeTaskbarIcons(prev, next, play, pause)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,12 +38,14 @@
|
|||||||
"Autoplay": "Autoplay",
|
"Autoplay": "Autoplay",
|
||||||
"Autoselect device": "Autoselect device",
|
"Autoselect device": "Autoselect device",
|
||||||
"Back": "Back",
|
"Back": "Back",
|
||||||
|
"Bit depth": "Bit depth",
|
||||||
"Bit rate": "Bit rate",
|
"Bit rate": "Bit rate",
|
||||||
"Bold font": "Bold font",
|
"Bold font": "Bold font",
|
||||||
"BPM": "BPM",
|
"BPM": "BPM",
|
||||||
"Broadcast": "Broadcast",
|
"Broadcast": "Broadcast",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Cast to device": "Cast to device",
|
"Cast to device": "Cast to device",
|
||||||
|
"Channels": "Channels",
|
||||||
"Check for Updates": "Check for Updates",
|
"Check for Updates": "Check for Updates",
|
||||||
"Close": "Close",
|
"Close": "Close",
|
||||||
"Close to system tray": "Close to system tray",
|
"Close to system tray": "Close to system tray",
|
||||||
@@ -175,6 +177,7 @@
|
|||||||
"ReplayGain mode": "ReplayGain mode",
|
"ReplayGain mode": "ReplayGain mode",
|
||||||
"ReplayGain preamp": "ReplayGain preamp",
|
"ReplayGain preamp": "ReplayGain preamp",
|
||||||
"Restart required": "Restart required",
|
"Restart required": "Restart required",
|
||||||
|
"Sample rate": "Sample rate",
|
||||||
"Save play queue on exit": "Save play queue on exit",
|
"Save play queue on exit": "Save play queue on exit",
|
||||||
"Saved at": "Saved at",
|
"Saved at": "Saved at",
|
||||||
"Scrobble when": "Scrobble when",
|
"Scrobble when": "Scrobble when",
|
||||||
@@ -213,6 +216,7 @@
|
|||||||
"Soundtrack": "Soundtrack",
|
"Soundtrack": "Soundtrack",
|
||||||
"Spoken Word": "Spoken Word",
|
"Spoken Word": "Spoken Word",
|
||||||
"Startup page": "Startup page",
|
"Startup page": "Startup page",
|
||||||
|
"Stop after current track": "Stop after current track",
|
||||||
"Stopped": "Stopped",
|
"Stopped": "Stopped",
|
||||||
"Success": "Success",
|
"Success": "Success",
|
||||||
"Support the project": "Support the project",
|
"Support the project": "Support the project",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package wintaskbarthumbs
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed media_pause.png
|
||||||
|
var MediaPausePNG []byte
|
||||||
|
|
||||||
|
//go:embed media_play.png
|
||||||
|
var MediaPlayPNG []byte
|
||||||
|
|
||||||
|
//go:embed media_seek_next.png
|
||||||
|
var MediaSeekNextPNG []byte
|
||||||
|
|
||||||
|
//go:embed media_seek_previous.png
|
||||||
|
var MediaSeekPreviousPNG []byte
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 376 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -27,6 +27,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/lang"
|
"fyne.io/fyne/v2/lang"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
@@ -57,6 +58,7 @@ type Controller struct {
|
|||||||
|
|
||||||
popUpQueue *widget.PopUp
|
popUpQueue *widget.PopUp
|
||||||
popUpQueueList *widgets.PlayQueueList
|
popUpQueueList *widgets.PlayQueueList
|
||||||
|
stopAfterCurrent *widget.Check
|
||||||
popUpQueueLastUsed int64
|
popUpQueueLastUsed int64
|
||||||
escapablePopUp fyne.CanvasObject
|
escapablePopUp fyne.CanvasObject
|
||||||
haveModal bool
|
haveModal bool
|
||||||
@@ -195,7 +197,11 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
|||||||
title := widget.NewRichTextWithText(lang.L("Play Queue"))
|
title := widget.NewRichTextWithText(lang.L("Play Queue"))
|
||||||
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
||||||
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
|
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
|
||||||
ctr := container.NewBorder(title, nil, nil, nil,
|
m.stopAfterCurrent = widget.NewCheck(lang.L("Stop after current track"), func(b bool) {
|
||||||
|
m.App.PlaybackManager.SetStopAfterCurrent(b)
|
||||||
|
})
|
||||||
|
bottomRow := container.NewHBox(layout.NewSpacer(), m.stopAfterCurrent)
|
||||||
|
ctr := container.NewBorder(title, bottomRow, nil, nil,
|
||||||
container.NewPadded(m.popUpQueueList),
|
container.NewPadded(m.popUpQueueList),
|
||||||
)
|
)
|
||||||
m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas())
|
m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas())
|
||||||
@@ -224,6 +230,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
|||||||
fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue)
|
fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue)
|
||||||
m.popUpQueue = nil
|
m.popUpQueue = nil
|
||||||
m.popUpQueueList = nil
|
m.popUpQueueList = nil
|
||||||
|
m.stopAfterCurrent = nil
|
||||||
m.popUpQueueLastUsed = 0
|
m.popUpQueueLastUsed = 0
|
||||||
t.Stop()
|
t.Stop()
|
||||||
return
|
return
|
||||||
@@ -252,6 +259,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
|||||||
))
|
))
|
||||||
pop.Resize(size)
|
pop.Resize(size)
|
||||||
popUpQueueList.ScrollToNowPlaying() // must come after resize
|
popUpQueueList.ScrollToNowPlaying() // must come after resize
|
||||||
|
m.stopAfterCurrent.SetChecked(m.App.PlaybackManager.IsStopAfterCurrent())
|
||||||
pop.ShowAtPosition(fyne.NewPos(
|
pop.ShowAtPosition(fyne.NewPos(
|
||||||
canvasSize.Width-size.Width-10,
|
canvasSize.Width-size.Width-10,
|
||||||
canvasSize.Height-size.Height-100,
|
canvasSize.Height-size.Height-100,
|
||||||
|
|||||||
@@ -105,6 +105,15 @@ func (t *TrackInfoDialog) CreateRenderer() fyne.WidgetRenderer {
|
|||||||
|
|
||||||
addFormRow(c, lang.L("Content type"), t.track.ContentType)
|
addFormRow(c, lang.L("Content type"), t.track.ContentType)
|
||||||
addFormRow(c, lang.L("Bit rate"), fmt.Sprintf("%d kbps", t.track.BitRate))
|
addFormRow(c, lang.L("Bit rate"), fmt.Sprintf("%d kbps", t.track.BitRate))
|
||||||
|
if t.track.SampleRate > 0 {
|
||||||
|
addFormRow(c, lang.L("Sample rate"), fmt.Sprintf("%d Hz", t.track.SampleRate))
|
||||||
|
}
|
||||||
|
if t.track.BitDepth > 0 {
|
||||||
|
addFormRow(c, lang.L("Bit depth"), strconv.Itoa(t.track.BitDepth))
|
||||||
|
}
|
||||||
|
if t.track.Channels > 0 {
|
||||||
|
addFormRow(c, lang.L("Channels"), strconv.Itoa(t.track.Channels))
|
||||||
|
}
|
||||||
addFormRow(c, lang.L("File size"), util.BytesToSizeString(t.track.Size))
|
addFormRow(c, lang.L("File size"), util.BytesToSizeString(t.track.Size))
|
||||||
addFormRow(c, lang.L("Play count"), strconv.Itoa(t.track.PlayCount))
|
addFormRow(c, lang.L("Play count"), strconv.Itoa(t.track.PlayCount))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user