Merge from main

This commit is contained in:
E1int
2025-08-09 23:39:44 +02:00
30 changed files with 593 additions and 50 deletions
+35 -11
View File
@@ -20,6 +20,7 @@ import (
"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/backend/windows"
"github.com/google/uuid"
"github.com/20after4/configdir"
@@ -49,7 +50,7 @@ type App struct {
LocalPlayer *mpv.Player
UpdateChecker UpdateChecker
MPRISHandler *MPRISHandler
WinSMTC *SMTC
WinSMTC *windows.SMTC
ipcServer ipc.IPCServer
LrcLibFetcher *LrcLibFetcher
@@ -381,7 +382,7 @@ func (a *App) setupMPRIS(mprisAppName string) {
}
func (a *App) SetupWindowsSMTC(hwnd uintptr) {
smtc, err := InitSMTCForWindow(hwnd)
smtc, err := windows.InitSMTCForWindow(hwnd)
if err != nil {
log.Printf("error initializing SMTC: %d", err)
return
@@ -389,17 +390,17 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) {
a.WinSMTC = smtc
smtc.UpdateMetadata(a.displayAppName, "")
smtc.OnButtonPressed(func(btn SMTCButton) {
smtc.OnButtonPressed(func(btn windows.SMTCButton) {
switch btn {
case SMTCButtonPlay:
case windows.SMTCButtonPlay:
a.PlaybackManager.Continue()
case SMTCButtonPause:
case windows.SMTCButtonPause:
a.PlaybackManager.Pause()
case SMTCButtonNext:
case windows.SMTCButtonNext:
a.PlaybackManager.SeekNext()
case SMTCButtonPrevious:
case windows.SMTCButtonPrevious:
a.PlaybackManager.SeekBackOrPrevious()
case SMTCButtonStop:
case windows.SMTCButtonStop:
a.PlaybackManager.Stop()
}
})
@@ -428,18 +429,37 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) {
})
a.PlaybackManager.OnPlaying(func() {
smtc.SetEnabled(true)
smtc.UpdatePlaybackState(SMTCPlaybackStatePlaying)
smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePlaying)
})
a.PlaybackManager.OnPaused(func() {
smtc.SetEnabled(true)
smtc.UpdatePlaybackState(SMTCPlaybackStatePaused)
smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePaused)
})
a.PlaybackManager.OnStopped(func() {
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 {
serverCfg := a.ServerManager.GetDefaultServer()
if serverCfg == nil {
@@ -551,6 +571,10 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
return cli.SeekBackOrPrevious()
case *FlagNext:
return cli.SeekNext()
case *FlagStop:
return cli.Stop()
case *FlagStopAfterCurrent:
return cli.StopAfterCurrent()
case *FlagShow:
return cli.Show()
case VolumeCLIArg >= 0:
+12 -10
View File
@@ -22,16 +22,18 @@ var (
SearchPlaylistCLIArg string = ""
SearchTrackCLIArg string = ""
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
FlagPause = flag.Bool("pause", false, "pause playback")
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
FlagNext = flag.Bool("next", false, "seek to next track")
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
FlagShow = flag.Bool("show", false, "show minimized app")
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")
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
FlagPause = flag.Bool("pause", false, "pause playback")
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
FlagNext = flag.Bool("next", false, "seek to next track")
FlagStop = flag.Bool("stop", false, "stop playback")
FlagStopAfterCurrent = flag.Bool("stop-after-current", false, "stop playback after current track")
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
FlagShow = flag.Bool("show", false, "show minimized app")
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
FlagPlayPlaylist *bool
+20 -19
View File
@@ -7,25 +7,26 @@ import (
)
const (
PingPath = "/ping"
PlayPath = "/transport/play"
PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle>
PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle>
PlayTrackPath = "/transport/play-track" // ?id=<track ID>
SearchAlbumPath = "/transport/search-album" // ?s=<searchQuery>
SearchPlaylistPath = "/transport/search-playlist" // ?s=<searchQuery>
SearchTrackPath = "/transport/search-track" // ?s=<searchQuery>
PlayPausePath = "/transport/playpause"
PausePath = "/transport/pause"
StopPath = "/transport/stop"
PreviousPath = "/transport/previous"
NextPath = "/transport/next"
TimePosPath = "/transport/timepos" // ?s=<seconds>
SeekByPath = "/transport/seek-by" // ?s=<+/- seconds>
VolumePath = "/volume" // ?v=<vol>
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
ShowPath = "/window/show"
QuitPath = "/window/quit"
PingPath = "/ping"
PlayPath = "/transport/play"
PlayAlbumPath = "/transport/play-album" // ?id=<album ID>&t=<firstTrack>&s=<shuffle>
PlayPlaylistPath = "/transport/play-playlist" // ?id=<playlist ID>&t=<firstTrack>&s=<shuffle>
PlayTrackPath = "/transport/play-track" // ?id=<track ID>
SearchAlbumPath = "/transport/search-album" // ?s=<searchQuery>
SearchPlaylistPath = "/transport/search-playlist" // ?s=<searchQuery>
SearchTrackPath = "/transport/search-track" // ?s=<searchQuery>
PlayPausePath = "/transport/playpause"
PausePath = "/transport/pause"
StopPath = "/transport/stop"
StopAfterCurrentPath = "/transport/stop-after-current"
PreviousPath = "/transport/previous"
NextPath = "/transport/next"
TimePosPath = "/transport/timepos" // ?s=<seconds>
SeekByPath = "/transport/seek-by" // ?s=<+/- seconds>
VolumePath = "/volume" // ?v=<vol>
VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage>
ShowPath = "/window/show"
QuitPath = "/window/quit"
)
type Response struct {
+10
View File
@@ -76,6 +76,16 @@ func (c *Client) PlayPause() error {
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 {
_, err := c.sendRequest(NextPath)
return err
+4
View File
@@ -18,6 +18,7 @@ type PlaybackHandler interface {
Continue()
SeekBackOrPrevious()
SeekNext()
SetStopAfterCurrent(bool)
SeekSeconds(float64)
SeekBySeconds(float64)
Volume() int
@@ -75,6 +76,9 @@ func (s *serverImpl) createHandler() http.Handler {
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause))
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(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
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.Size = int64(ch.MediaSources[0].Size)
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
}
+3
View File
@@ -149,6 +149,9 @@ type Track struct {
Comment string
BPM int
ReplayGain ReplayGainInfo
SampleRate int
BitDepth int
Channels int
}
type ReplayGainInfo struct {
@@ -573,6 +573,9 @@ func toTrack(ch *subsonic.Child) *mediaprovider.Track {
Comment: ch.Comment,
BPM: ch.BPM,
ReplayGain: rGain,
SampleRate: ch.SamplingRate,
BitDepth: ch.BitDepth,
Channels: ch.ChannelCount,
}
}
+13 -1
View File
@@ -65,6 +65,8 @@ type playbackEngine struct {
isRadio bool
loopMode LoopMode
stopAfterCurrent bool // flag to stop playback after current track ends
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
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)
}
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 {
return p.player.Pause()
}
@@ -634,6 +645,7 @@ func (p *playbackEngine) handleOnStopped() {
p.alreadyScrobbled = false
p.wasStopped = true
p.nowPlayingIdx = -1
p.stopAfterCurrent = false
}
// 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()
}
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.setNextTrack(p.nextPlayingIndex())
}
+8
View File
@@ -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() {
nowPlaying := p.NowPlaying()
if nowPlaying == nil {
+3
View File
@@ -229,6 +229,9 @@ func (d *DLNAPlayer) SetNextFile(url string, meta mediaprovider.MediaItemMetadat
Title: meta.Name,
Seekable: true,
}
} else {
// empty media item to signify erasing next track in device queue
media = &avtransport.MediaItem{}
}
ctx, cancel := context.WithCancel(context.Background())
+1 -1
View File
@@ -1,6 +1,6 @@
//go:build windows
package backend
package windows
/*
#cgo CFLAGS: -I .
@@ -1,6 +1,6 @@
//go:build windows
package backend
package windows
/*
void btn_callback_cgo(int in) {
@@ -1,6 +1,6 @@
//go:build !windows
package backend
package windows
import "errors"
+175
View File
@@ -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;
}
+148
View File
@@ -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
}
+26
View File
@@ -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
}