mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2025-12-06 10:22:50 +08:00
Compare commits
13 Commits
6b4fbf6951
...
abaa0a9d5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abaa0a9d5c | ||
|
|
d06ee8f61e | ||
|
|
5b0c7ae5f2 | ||
|
|
135c022cec | ||
|
|
8b643cd004 | ||
|
|
3c8c8f3834 | ||
|
|
f070ee3f47 | ||
|
|
f59aebd2f8 | ||
|
|
650da87f64 | ||
|
|
2838a02c83 | ||
|
|
5c508b9664 | ||
|
|
7c3f8587f6 | ||
|
|
918e2e81b3 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -57,10 +57,6 @@ jobs:
|
||||
go mod tidy
|
||||
go install fyne.io/tools/cmd/fyne@latest
|
||||
|
||||
- name: Bundle assets
|
||||
run: |
|
||||
fyne bundle --name resImageIcon --package resource ./assets/icon2.png > ./resource/bundle.go
|
||||
|
||||
- name: Build application
|
||||
run: |
|
||||
go build -tags="mpvOnly,nosource" -v -o ./AynaLivePlayerMpvNoSource.exe -ldflags -H=windowsgui app/main.go
|
||||
@@ -118,10 +114,6 @@ jobs:
|
||||
go mod tidy
|
||||
go install fyne.io/tools/cmd/fyne@latest
|
||||
|
||||
- name: Bundle assets
|
||||
run: |
|
||||
fyne bundle --name resImageIcon --package resource ./assets/icon.png > ./resource/bundle.go
|
||||
|
||||
- name: Build application
|
||||
run: go build -o ./${{ env.EXECUTABLE }} app/main.go
|
||||
|
||||
|
||||
32
app/main.go
32
app/main.go
@@ -1,19 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/internal"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
|
||||
loggerRepo "AynaLivePlayer/pkg/logger/repository"
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var dev = flag.Bool("dev", false, "dev")
|
||||
@@ -40,13 +43,23 @@ var Log = &_LogConfig{
|
||||
|
||||
func setupGlobal() {
|
||||
//global.EventManager = event.NewManger(128, 16)
|
||||
global.EventBus = eventbus.New()
|
||||
global.EventBus = eventbus.New(eventbus.WithMaxWorkerSize(len(events.EventsMapping)))
|
||||
global.Logger = loggerRepo.NewZapColoredLogger(Log.Path, !*dev)
|
||||
global.Logger.SetLogLevel(Log.Level)
|
||||
}
|
||||
|
||||
func main() {
|
||||
func init() {
|
||||
flag.Parse()
|
||||
// if not dev, set working directory to executable directory
|
||||
if !*dev {
|
||||
exePath, _ := os.Executable()
|
||||
exePath, _ = filepath.EvalSymlinks(exePath)
|
||||
exeDir := filepath.Dir(exePath)
|
||||
_ = os.Chdir(exeDir)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadFromFile(config.ConfigPath)
|
||||
config.LoadConfig(Log)
|
||||
i18n.LoadLanguage(config.General.Language)
|
||||
@@ -54,26 +67,21 @@ func main() {
|
||||
global.Logger.Info("================Program Start================")
|
||||
global.Logger.Infof("================Current Version: %s================", model.Version(config.Version))
|
||||
internal.Initialize()
|
||||
go func() {
|
||||
// temporary fix for gui not render correctly.
|
||||
// wait until gui rendered then start event dispatching
|
||||
time.Sleep(1 * time.Second)
|
||||
//global.EventManager.Start()
|
||||
_ = global.EventBus.Start()
|
||||
}()
|
||||
if *headless || config.Experimental.Headless {
|
||||
quit := make(chan os.Signal)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
_ = global.EventBus.Start()
|
||||
<-quit
|
||||
} else {
|
||||
gui.Initialize()
|
||||
gui.MainWindow.ShowAndRun()
|
||||
_ = global.EventBus.Start()
|
||||
gctx.Context.Window.ShowAndRun()
|
||||
}
|
||||
global.Logger.Info("closing internal server")
|
||||
internal.Stop()
|
||||
global.Logger.Infof("closing event manager")
|
||||
//global.EventManager.Stop()
|
||||
_ = global.EventBus.Stop()
|
||||
_ = global.EventBus.Wait()
|
||||
if *dev {
|
||||
global.Logger.Infof("saving translation")
|
||||
i18n.SaveTranslation()
|
||||
|
||||
BIN
assets/msyh.ttc
BIN
assets/msyh.ttc
Binary file not shown.
BIN
assets/msyh0.ttf
BIN
assets/msyh0.ttf
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -556,37 +556,37 @@
|
||||
"en": "Obs browser output",
|
||||
"zh-CN": "OBS网页输出: "
|
||||
},
|
||||
"plugin.yinliang.title": {
|
||||
"en": "Volume Control",
|
||||
"zh-CN": "音量控制"
|
||||
"plugin.yinliang.admin_permission": {
|
||||
"en": "Admin only",
|
||||
"zh-CN": "仅房管可操作"
|
||||
},
|
||||
"plugin.yinliang.description": {
|
||||
"en": "Control volume via danmaku",
|
||||
"zh-CN": "通过弹幕控制音量"
|
||||
},
|
||||
"plugin.yinliang.admin_permission": {
|
||||
"en": "Admin only",
|
||||
"zh-CN": "仅房管可操作"
|
||||
},
|
||||
"plugin.yinliang.enabled": {
|
||||
"en": "Enabled volume control",
|
||||
"zh-CN": "启用弹幕音量控制"
|
||||
},
|
||||
"plugin.yinliang.volume_up_cmd": {
|
||||
"en": "Volume increase command",
|
||||
"zh-CN": "音量增加命令"
|
||||
"plugin.yinliang.max_volume": {
|
||||
"en": "Maximum volume (%)",
|
||||
"zh-CN": "最大音量限制 (%)"
|
||||
},
|
||||
"plugin.yinliang.title": {
|
||||
"en": "Volume Control",
|
||||
"zh-CN": "音量控制"
|
||||
},
|
||||
"plugin.yinliang.volume_down_cmd": {
|
||||
"en": "Volume decrease command",
|
||||
"en": "Volume decrease command",
|
||||
"zh-CN": "音量减少命令"
|
||||
},
|
||||
"plugin.yinliang.volume_step": {
|
||||
"en": "Adjustment step (%)",
|
||||
"zh-CN": "每次音量调整 (%)"
|
||||
},
|
||||
"plugin.yinliang.max_volume": {
|
||||
"en": "Maximum volume (%)",
|
||||
"zh-CN": "最大音量限制 (%)"
|
||||
"plugin.yinliang.volume_up_cmd": {
|
||||
"en": "Volume increase command",
|
||||
"zh-CN": "音量增加命令"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ type ErrorUpdateEvent struct {
|
||||
// Value model.PlayerPropertyValue
|
||||
//}
|
||||
//
|
||||
//type LiveRoomStatusUpdateEvent struct {
|
||||
//type UpdateLiveRoomStatusData struct {
|
||||
// RoomTitle string
|
||||
// Status bool
|
||||
//}
|
||||
|
||||
25
core/events/events.go
Normal file
25
core/events/events.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package events
|
||||
|
||||
/*
|
||||
# events package
|
||||
|
||||
events package contains all events used in application.
|
||||
|
||||
in theory. all interaction should use events package.
|
||||
|
||||
the events are dispatched using eventbus package.
|
||||
|
||||
Here are some major events
|
||||
|
||||
- cmd: call cmd
|
||||
- reply: call reply
|
||||
- update: information updating event. usually issued by internal controller and broadcast to all channel
|
||||
|
||||
|
||||
naming convention
|
||||
|
||||
- cmd: 'cmd.event.id.'
|
||||
- reply: 'reply.same.same.cmd.id'
|
||||
- update: 'update.event.id'
|
||||
|
||||
*/
|
||||
@@ -5,65 +5,56 @@ import (
|
||||
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
|
||||
)
|
||||
|
||||
//const (
|
||||
// LiveRoomStatusChange string = "liveclient.status.change"
|
||||
// LiveRoomMessageReceive string = "liveclient.message.receive"
|
||||
//)
|
||||
//
|
||||
//type StatusChangeEvent struct {
|
||||
// Connected bool
|
||||
// Client adapter.LiveClient
|
||||
//}
|
||||
const CmdLiveRoomAdd = "cmd.liveroom.add"
|
||||
|
||||
const LiveRoomAddCmd = "cmd.liveroom.add"
|
||||
|
||||
type LiveRoomAddCmdEvent struct {
|
||||
type CmdLiveRoomAddData struct {
|
||||
Title string
|
||||
Provider string
|
||||
RoomKey string
|
||||
}
|
||||
|
||||
const CmdLiveRoomRemove = "cmd.liveroom.remove"
|
||||
|
||||
type CmdLiveRoomRemoveData struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
const CmdLiveRoomConfigChange = "cmd.liveroom.config.change"
|
||||
|
||||
type CmdLiveRoomConfigChangeData struct {
|
||||
Identifier string
|
||||
Config model.LiveRoomConfig
|
||||
}
|
||||
|
||||
const LiveRoomProviderUpdate = "update.liveroom.provider"
|
||||
|
||||
type LiveRoomProviderUpdateEvent struct {
|
||||
Providers []model.LiveRoomProviderInfo
|
||||
}
|
||||
|
||||
const LiveRoomRemoveCmd = "cmd.liveroom.remove"
|
||||
const UpdateLiveRoomRooms = "update.liveroom.rooms"
|
||||
|
||||
type LiveRoomRemoveCmdEvent struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
const LiveRoomRoomsUpdate = "update.liveroom.rooms"
|
||||
|
||||
type LiveRoomRoomsUpdateEvent struct {
|
||||
type UpdateLiveRoomRoomsData struct {
|
||||
Rooms []model.LiveRoom
|
||||
}
|
||||
|
||||
const LiveRoomStatusUpdate = "update.liveroom.status"
|
||||
const UpdateLiveRoomStatus = "update.liveroom.status"
|
||||
|
||||
type LiveRoomStatusUpdateEvent struct {
|
||||
type UpdateLiveRoomStatusData struct {
|
||||
Room model.LiveRoom
|
||||
}
|
||||
|
||||
const LiveRoomConfigChangeCmd = "cmd.liveroom.config.change"
|
||||
const CmdLiveRoomOperation = "cmd.liveroom.operation"
|
||||
|
||||
type LiveRoomConfigChangeCmdEvent struct {
|
||||
Identifier string
|
||||
Config model.LiveRoomConfig
|
||||
}
|
||||
|
||||
const LiveRoomOperationCmd = "cmd.liveroom.operation"
|
||||
|
||||
type LiveRoomOperationCmdEvent struct {
|
||||
type CmdLiveRoomOperationData struct {
|
||||
Identifier string
|
||||
SetConnect bool // connect or disconnect
|
||||
}
|
||||
|
||||
const LiveRoomOperationFinish = "update.liveroom.operation"
|
||||
const ReplyLiveRoomOperation = "reply.liveroom.operation"
|
||||
|
||||
type LiveRoomOperationFinishEvent struct {
|
||||
type ReplyLiveRoomOperationData struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
const LiveRoomMessageReceive = "update.liveroom.message"
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
)
|
||||
|
||||
var EventsMapping = map[string]any{
|
||||
LiveRoomAddCmd: LiveRoomAddCmdEvent{},
|
||||
CmdLiveRoomAdd: CmdLiveRoomAddData{},
|
||||
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
|
||||
LiveRoomRemoveCmd: LiveRoomRemoveCmdEvent{},
|
||||
LiveRoomRoomsUpdate: LiveRoomRoomsUpdateEvent{},
|
||||
LiveRoomStatusUpdate: LiveRoomStatusUpdateEvent{},
|
||||
LiveRoomConfigChangeCmd: LiveRoomConfigChangeCmdEvent{},
|
||||
LiveRoomOperationCmd: LiveRoomOperationCmdEvent{},
|
||||
CmdLiveRoomRemove: CmdLiveRoomRemoveData{},
|
||||
UpdateLiveRoomRooms: UpdateLiveRoomRoomsData{},
|
||||
UpdateLiveRoomStatus: UpdateLiveRoomStatusData{},
|
||||
CmdLiveRoomConfigChange: CmdLiveRoomConfigChangeData{},
|
||||
CmdLiveRoomOperation: CmdLiveRoomOperationData{},
|
||||
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
|
||||
PlayerPlayCmd: PlayerPlayCmdEvent{},
|
||||
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
|
||||
@@ -22,8 +22,8 @@ var EventsMapping = map[string]any{
|
||||
PlayerToggleCmd: PlayerToggleCmdEvent{},
|
||||
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
|
||||
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
|
||||
PlayerLyricRequestCmd: PlayerLyricRequestCmdEvent{},
|
||||
PlayerLyricReload: PlayerLyricReloadEvent{},
|
||||
CmdGetCurrentLyric: CmdGetCurrentLyricData{},
|
||||
UpdateCurrentLyric: UpdateCurrentLyricData{},
|
||||
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
|
||||
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
|
||||
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
|
||||
@@ -44,13 +44,13 @@ var EventsMapping = map[string]any{
|
||||
PlaylistManagerAddPlaylistCmd: PlaylistManagerAddPlaylistCmdEvent{},
|
||||
PlaylistManagerRemovePlaylistCmd: PlaylistManagerRemovePlaylistCmdEvent{},
|
||||
MediaProviderUpdate: MediaProviderUpdateEvent{},
|
||||
SearchCmd: SearchCmdEvent{},
|
||||
SearchResultUpdate: SearchResultUpdateEvent{},
|
||||
CmdMiaosicSearch: CmdMiaosicSearchData{},
|
||||
ReplyMiaosicSearch: ReplyMiaosicSearchData{},
|
||||
GUISetPlayerWindowOpenCmd: GUISetPlayerWindowOpenCmdEvent{},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer, model.PlaylistIDHistory} {
|
||||
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer} {
|
||||
EventsMapping[PlaylistDetailUpdate(v)] = PlaylistDetailUpdateEvent{}
|
||||
EventsMapping[PlaylistMoveCmd(v)] = PlaylistMoveCmdEvent{}
|
||||
EventsMapping[PlaylistSetIndexCmd(v)] = PlaylistSetIndexCmdEvent{}
|
||||
|
||||
@@ -7,16 +7,16 @@ import (
|
||||
)
|
||||
|
||||
func TestUnmarshalEventData(t *testing.T) {
|
||||
eventData := LiveRoomAddCmdEvent{
|
||||
eventData := CmdLiveRoomAddData{
|
||||
Title: "test",
|
||||
Provider: "asdfasd",
|
||||
RoomKey: "asdfasdf",
|
||||
}
|
||||
data, err := json.Marshal(eventData)
|
||||
require.NoError(t, err)
|
||||
val, err := UnmarshalEventData(LiveRoomAddCmd, data)
|
||||
val, err := UnmarshalEventData(CmdLiveRoomAdd, data)
|
||||
require.NoError(t, err)
|
||||
resultData, ok := val.(LiveRoomAddCmdEvent)
|
||||
resultData, ok := val.(CmdLiveRoomAddData)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, eventData, resultData)
|
||||
}
|
||||
|
||||
56
core/events/miaosic.go
Normal file
56
core/events/miaosic.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package events
|
||||
|
||||
import "github.com/AynaLivePlayer/miaosic"
|
||||
|
||||
const CmdMiaosicGetMediaInfo = "cmd.miaosic.getMediaInfo"
|
||||
|
||||
type CmdMiaosicGetMediaInfoData struct {
|
||||
Meta miaosic.MetaData `json:"meta"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicGetMediaInfo = "reply.miaosic.getMediaInfo"
|
||||
|
||||
type ReplyMiaosicGetMediaInfoData struct {
|
||||
Info miaosic.MediaInfo `json:"info"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
const CmdMiaosicGetMediaUrl = "cmd.miaosic.getMediaUrl"
|
||||
|
||||
type CmdMiaosicGetMediaUrlData struct {
|
||||
Meta miaosic.MetaData `json:"meta"`
|
||||
Quality miaosic.Quality `json:"quality"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicGetMediaUrl = "reply.miaosic.getMediaUrl"
|
||||
|
||||
type ReplyMiaosicGetMediaUrlData struct {
|
||||
Urls []miaosic.MediaUrl `json:"urls"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
const CmdMiaosicQrLogin = "cmd.miaosic.qrLogin"
|
||||
|
||||
type CmdMiaosicQrLoginData struct {
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicQrLogin = "reply.miaosic.qrLogin"
|
||||
|
||||
type ReplyMiaosicQrLoginData struct {
|
||||
Session miaosic.QrLoginSession `json:"session"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
const CmdMiaosicQrLoginVerify = "cmd.miaosic.qrLoginVerify"
|
||||
|
||||
type CmdMiaosicQrLoginVerifyData struct {
|
||||
Session miaosic.QrLoginSession `json:"session"`
|
||||
}
|
||||
|
||||
const ReplyMiaosicQrLoginVerify = "reply.miaosic.qrLoginVerify"
|
||||
|
||||
type ReplyMiaosicQrLoginVerifyData struct {
|
||||
Result miaosic.QrLoginResult `json:"result"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
@@ -2,14 +2,14 @@ package events
|
||||
|
||||
import "github.com/AynaLivePlayer/miaosic"
|
||||
|
||||
const PlayerLyricRequestCmd = "cmd.player.lyric.request"
|
||||
const CmdGetCurrentLyric = "cmd.player.lyric.request"
|
||||
|
||||
type PlayerLyricRequestCmdEvent struct {
|
||||
type CmdGetCurrentLyricData struct {
|
||||
}
|
||||
|
||||
const PlayerLyricReload = "update.player.lyric.reload"
|
||||
const UpdateCurrentLyric = "update.player.lyric.reload"
|
||||
|
||||
type PlayerLyricReloadEvent struct {
|
||||
type UpdateCurrentLyricData struct {
|
||||
Lyrics miaosic.Lyrics
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ import (
|
||||
"AynaLivePlayer/core/model"
|
||||
)
|
||||
|
||||
const SearchCmd = "cmd.search"
|
||||
const CmdMiaosicSearch = "cmd.search"
|
||||
|
||||
type SearchCmdEvent struct {
|
||||
type CmdMiaosicSearchData struct {
|
||||
Keyword string
|
||||
Provider string
|
||||
}
|
||||
|
||||
const SearchResultUpdate = "update.search_result"
|
||||
const ReplyMiaosicSearch = "update.search_result"
|
||||
|
||||
type SearchResultUpdateEvent struct {
|
||||
type ReplyMiaosicSearchData struct {
|
||||
Medias []model.Media
|
||||
}
|
||||
|
||||
@@ -13,9 +13,8 @@ const (
|
||||
type PlaylistID string
|
||||
|
||||
const (
|
||||
PlaylistIDPlayer PlaylistID = "player"
|
||||
PlaylistIDSystem PlaylistID = "system"
|
||||
PlaylistIDHistory PlaylistID = "history"
|
||||
PlaylistIDPlayer PlaylistID = "player"
|
||||
PlaylistIDSystem PlaylistID = "system"
|
||||
)
|
||||
|
||||
type PlaylistInfo struct {
|
||||
|
||||
3
go.mod
3
go.mod
@@ -8,7 +8,7 @@ replace (
|
||||
github.com/AynaLivePlayer/liveroom-sdk v0.1.0 => ./pkg/liveroom-sdk // submodule
|
||||
github.com/AynaLivePlayer/miaosic v0.2.3 => ./pkg/miaosic // submodule
|
||||
|
||||
github.com/saltosystems/winrt-go => github.com/go-musicfox/winrt-go v0.1.4 // winrt with media foundation
|
||||
github.com/saltosystems/winrt-go => github.com/AynaLivePlayer/winrt-go v0.0.0-20250902062117-902ba325aee0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -23,6 +23,7 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/k0kubun/pp/v3 v3.5.0
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff
|
||||
|
||||
4
go.sum
4
go.sum
@@ -4,6 +4,8 @@ fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlF
|
||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc h1:t1fMdqUjB2lR9uuGQ9yWJ7LJ3h1hXhI+LhbTpElPueI=
|
||||
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc/go.mod h1:u+JfexgX5pYrylIuC5zP3N/Ylp47K/xvl+ntpZtosuE=
|
||||
github.com/AynaLivePlayer/winrt-go v0.0.0-20250902062117-902ba325aee0 h1:orjVC4k6/CU7279G9abWaBIIiCgxUpDhkaM24o7arvs=
|
||||
github.com/AynaLivePlayer/winrt-go v0.0.0-20250902062117-902ba325aee0/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -56,8 +58,6 @@ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
|
||||
github.com/go-musicfox/winrt-go v0.1.4 h1:xg+7VKsIozGK8S4X4zNQ/3HNhg5yHWYaTE+Zs4jySaU=
|
||||
github.com/go-musicfox/winrt-go v0.1.4/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
@@ -15,7 +14,6 @@ type Entry struct {
|
||||
func NewEntry() *Entry {
|
||||
e := &Entry{}
|
||||
e.ExtendBaseWidget(e)
|
||||
xfyne.EntryDisableUndoRedo(&e.Entry)
|
||||
return e
|
||||
}
|
||||
|
||||
|
||||
22
gui/component/fixedsize.go
Normal file
22
gui/component/fixedsize.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type LabelFixedSize struct {
|
||||
*widget.Label
|
||||
fixedSize fyne.Size
|
||||
}
|
||||
|
||||
func (t *LabelFixedSize) MinSize() fyne.Size {
|
||||
return t.fixedSize
|
||||
}
|
||||
|
||||
func NewLabelFixedSize(label *widget.Label) *LabelFixedSize {
|
||||
return &LabelFixedSize{
|
||||
Label: label,
|
||||
fixedSize: label.MinSize(),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package gui
|
||||
package component
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
40
gui/component/label.go
Normal file
40
gui/component/label.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type LabelOpt func(*widget.Label)
|
||||
|
||||
func LabelWrapping(wrapping fyne.TextWrap) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.Wrapping = wrapping
|
||||
}
|
||||
}
|
||||
|
||||
func LabelAlignment(align fyne.TextAlign) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.Alignment = align
|
||||
}
|
||||
}
|
||||
|
||||
func LabelTextStyle(style fyne.TextStyle) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.TextStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
func LabelTruncation(truncation fyne.TextTruncation) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.Truncation = truncation
|
||||
}
|
||||
}
|
||||
|
||||
func NewLabelWithOpts(text string, opts ...LabelOpt) *widget.Label {
|
||||
l := widget.NewLabel(text)
|
||||
for _, opt := range opts {
|
||||
opt(l)
|
||||
}
|
||||
return l
|
||||
}
|
||||
28
gui/component/lyrics/LICENSE.txt
Normal file
28
gui/component/lyrics/LICENSE.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Drew Weymouth
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
97
gui/component/lyrics/lyricline.go
Normal file
97
gui/component/lyrics/lyricline.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package lyrics
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type lyricLine struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Text string
|
||||
SizeName fyne.ThemeSizeName
|
||||
ColorName fyne.ThemeColorName
|
||||
HoveredColorName fyne.ThemeColorName
|
||||
Alignment fyne.TextAlign
|
||||
Tappable bool
|
||||
|
||||
onTapped func()
|
||||
hovered bool
|
||||
richtext *widget.RichText
|
||||
}
|
||||
|
||||
func newLyricLine(text string, onTapped func()) *lyricLine {
|
||||
l := &lyricLine{
|
||||
Text: text,
|
||||
SizeName: theme.SizeNameSubHeadingText,
|
||||
ColorName: theme.ColorNameForeground,
|
||||
Alignment: fyne.TextAlignLeading,
|
||||
onTapped: onTapped,
|
||||
}
|
||||
l.ExtendBaseWidget(l)
|
||||
return l
|
||||
}
|
||||
|
||||
var _ desktop.Hoverable = (*lyricLine)(nil)
|
||||
|
||||
func (l *lyricLine) MouseIn(*desktop.MouseEvent) {
|
||||
if l.Tappable {
|
||||
l.hovered = true
|
||||
l.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lyricLine) MouseMoved(*desktop.MouseEvent) {
|
||||
}
|
||||
|
||||
func (l *lyricLine) MouseOut() {
|
||||
l.hovered = false
|
||||
l.Refresh()
|
||||
}
|
||||
|
||||
var _ desktop.Cursorable = (*lyricLine)(nil)
|
||||
|
||||
func (l *lyricLine) Cursor() desktop.Cursor {
|
||||
if l.Tappable {
|
||||
return desktop.PointerCursor
|
||||
}
|
||||
return desktop.DefaultCursor
|
||||
}
|
||||
|
||||
var _ fyne.Tappable = (*lyricLine)(nil)
|
||||
|
||||
func (l *lyricLine) Tapped(*fyne.PointEvent) {
|
||||
if l.Tappable {
|
||||
l.onTapped()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lyricLine) updateRichText() {
|
||||
if l.richtext == nil {
|
||||
l.richtext = widget.NewRichText(&widget.TextSegment{
|
||||
Style: widget.RichTextStyleSubHeading,
|
||||
})
|
||||
l.richtext.Wrapping = fyne.TextWrapWord
|
||||
}
|
||||
seg := l.richtext.Segments[0].(*widget.TextSegment)
|
||||
seg.Text = l.Text
|
||||
seg.Style.Alignment = l.Alignment
|
||||
if l.hovered {
|
||||
seg.Style.ColorName = l.HoveredColorName
|
||||
} else {
|
||||
seg.Style.ColorName = l.ColorName
|
||||
}
|
||||
seg.Style.SizeName = l.SizeName
|
||||
}
|
||||
|
||||
func (l *lyricLine) Refresh() {
|
||||
l.updateRichText()
|
||||
l.richtext.Refresh()
|
||||
}
|
||||
|
||||
func (l *lyricLine) CreateRenderer() fyne.WidgetRenderer {
|
||||
l.updateRichText()
|
||||
return widget.NewSimpleRenderer(l.richtext)
|
||||
}
|
||||
412
gui/component/lyrics/lyricsviewer.go
Normal file
412
gui/component/lyrics/lyricsviewer.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package lyrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type ActiveLyricPosition int
|
||||
|
||||
const (
|
||||
// ActiveLyricPositionMiddle positions the active lyric line in the middle of the widget
|
||||
ActiveLyricPositionMiddle ActiveLyricPosition = iota
|
||||
|
||||
// ActiveLyricPositionUpperMiddle positions the active lyric line
|
||||
// in the upper-middle of the widget, roughly 1/3 of the way down
|
||||
ActiveLyricPositionUpperMiddle
|
||||
)
|
||||
|
||||
// LyricsViewer is a widget for displaying song lyrics.
|
||||
// It supports synced and unsynced mode. In synced mode, the active line
|
||||
// is highlighted and the widget can advance to the next line
|
||||
// with an animated scroll. In unsynced mode all lyrics are shown
|
||||
// in the active color and the user is allowed to scroll freely.
|
||||
type LyricsViewer struct {
|
||||
widget.BaseWidget
|
||||
|
||||
// Alignment controls the text alignment of the lyric lines
|
||||
Alignment fyne.TextAlign
|
||||
|
||||
// TextSizeName is the theme size name that controls the size of the lyric lines.
|
||||
// Defaults to theme.SizeNameSubHeadingText.
|
||||
TextSizeName fyne.ThemeSizeName
|
||||
|
||||
// ActiveLyricColorName is the theme color name that the currently active
|
||||
// lyric line will be drawn in synced mode, or all lyrics in non-synced mode.
|
||||
// Defaults to theme.ColorNameForeground.
|
||||
ActiveLyricColorName fyne.ThemeColorName
|
||||
|
||||
// InactiveLyricColorName is the theme color name that the inactive lyric lines
|
||||
// will be drawn in synced mode. Defaults to theme.ColorNameDisabled.
|
||||
InactiveLyricColorName fyne.ThemeColorName
|
||||
|
||||
// HoveredLyricColorName is the theme color name that hovered lyric lines
|
||||
// will be drawn in synced mode when an OnLyricTapped callback is set.
|
||||
// Defaults to theme.ColorNameHover.
|
||||
HoveredLyricColorName fyne.ThemeColorName
|
||||
|
||||
// ActiveLyricPosition sets the vertical positioning of the active lyric line
|
||||
// in synced mode.
|
||||
ActiveLyricPosition ActiveLyricPosition
|
||||
|
||||
// OnLyricTapped sets a callback function that is invoked when a
|
||||
// synced lyric line is tapped. The line number is *one-indexed*.
|
||||
// Typically used to seek to the timecode of the given lyric.
|
||||
// When showing unsynced lyrics, or if this callback is unset,
|
||||
// the visual styling of the widget will not indicate interactivity.
|
||||
OnLyricTapped func(lineNum int)
|
||||
|
||||
lines []string
|
||||
synced bool
|
||||
|
||||
// one-indexed - 0 means before the first line
|
||||
// during an animation, currentLine is the line
|
||||
// that will be scrolled when the animation is finished
|
||||
currentLine int
|
||||
|
||||
prototypeLyricLineSize fyne.Size
|
||||
|
||||
scroll *container.Scroll
|
||||
vbox *fyne.Container
|
||||
|
||||
// nil when an animation is not currently running
|
||||
anim *fyne.Animation
|
||||
animStartOffset float32
|
||||
}
|
||||
|
||||
// NewLyricsViewer returns a new lyrics viewer.
|
||||
func NewLyricsViewer() *LyricsViewer {
|
||||
s := &LyricsViewer{}
|
||||
s.ExtendBaseWidget(s)
|
||||
s.prototypeLyricLineSize = s.newLyricLine("Hello...", 0, false).MinSize()
|
||||
return s
|
||||
}
|
||||
|
||||
// SetLyrics sets the lyrics and also resets the current line to 0 if synced.
|
||||
func (l *LyricsViewer) SetLyrics(lines []string, synced bool) {
|
||||
l.lines = lines
|
||||
l.synced = synced
|
||||
l.currentLine = 0
|
||||
if l.scroll != nil {
|
||||
if synced {
|
||||
l.scroll.Direction = container.ScrollNone
|
||||
} else {
|
||||
l.scroll.Direction = container.ScrollVerticalOnly
|
||||
}
|
||||
}
|
||||
l.updateContent()
|
||||
}
|
||||
|
||||
// SetCurrentLine sets the current line that the lyric viewer is scrolled to.
|
||||
// Argument is *one-indexed* - SetCurrentLine(0) means setting the scroll to be
|
||||
// before the first line. In unsynced mode this is a no-op. This function is
|
||||
// typically called when the user has seeked the playing song to a new position.
|
||||
func (l *LyricsViewer) SetCurrentLine(line int) {
|
||||
if line < 0 || line > len(l.lines) {
|
||||
// do not panic, just ignore invalid input
|
||||
return
|
||||
}
|
||||
if l.vbox == nil || !l.synced {
|
||||
l.currentLine = line
|
||||
return // renderer not created yet or unsynced mode
|
||||
}
|
||||
inactiveColor := l.inactiveLyricColor()
|
||||
if l.checkStopAnimation() && l.currentLine > 1 {
|
||||
// we were in the middle of animation
|
||||
// make sure prev line is right color
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine-1].(*lyricLine), inactiveColor, true)
|
||||
}
|
||||
if l.currentLine != 0 {
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine].(*lyricLine), inactiveColor, true)
|
||||
}
|
||||
l.currentLine = line
|
||||
if l.currentLine != 0 {
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine].(*lyricLine), l.activeLyricColor(), true)
|
||||
}
|
||||
l.scroll.Offset.Y = l.offsetForLine(l.currentLine)
|
||||
l.scroll.Refresh()
|
||||
}
|
||||
|
||||
// NextLine advances the lyric viewer to the next line with an animated scroll.
|
||||
// In unsynced mode this is a no-op.
|
||||
func (l *LyricsViewer) NextLine() {
|
||||
if l.vbox == nil || !l.synced {
|
||||
return // no renderer yet, or unsynced lyrics (no-op)
|
||||
}
|
||||
|
||||
if l.currentLine == len(l.lines) {
|
||||
return // already at last line
|
||||
}
|
||||
if l.checkStopAnimation() {
|
||||
// we were in the middle of animation - short-circuit it to completed
|
||||
// make sure prev and current lines are right color and scrolled to the end
|
||||
if l.currentLine > 1 {
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine-1].(*lyricLine), l.inactiveLyricColor(), true)
|
||||
}
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine].(*lyricLine), l.activeLyricColor(), true)
|
||||
l.scroll.Offset.Y = l.offsetForLine(l.currentLine)
|
||||
}
|
||||
l.currentLine++
|
||||
|
||||
var prevLine, nextLine *lyricLine
|
||||
if l.currentLine > 1 {
|
||||
prevLine = l.vbox.Objects[l.currentLine-1].(*lyricLine)
|
||||
}
|
||||
if l.currentLine <= len(l.lines) {
|
||||
nextLine = l.vbox.Objects[l.currentLine].(*lyricLine)
|
||||
}
|
||||
|
||||
l.setupScrollAnimation(prevLine, nextLine)
|
||||
l.anim.Start()
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) Refresh() {
|
||||
l.updateContent()
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) MinSize() fyne.Size {
|
||||
// overridden because NoScroll will have minSize encompass the full lyrics
|
||||
// note also that leaving this to the renderer MinSize, based on the
|
||||
// VBox with RichText lines inside Scroll, may lead to race conditions
|
||||
// (https://github.com/fyne-io/fyne/issues/4890)
|
||||
minHeight := l.prototypeLyricLineSize.Height*3 + theme.Padding()*2
|
||||
return fyne.NewSize(l.prototypeLyricLineSize.Width, minHeight)
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) Resize(size fyne.Size) {
|
||||
l.updateSpacerSize(size)
|
||||
l.BaseWidget.Resize(size)
|
||||
if l.vbox == nil {
|
||||
return // renderer not created yet
|
||||
}
|
||||
if l.anim == nil {
|
||||
l.scroll.Offset = fyne.NewPos(0, l.offsetForLine(l.currentLine))
|
||||
l.scroll.Refresh()
|
||||
} else {
|
||||
// animation is running - update its reference scroll pos
|
||||
l.animStartOffset = l.offsetForLine(l.currentLine - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) updateSpacerSize(size fyne.Size) {
|
||||
if l.vbox == nil {
|
||||
return // renderer not created yet
|
||||
}
|
||||
|
||||
ht := size.Height / 2
|
||||
if l.ActiveLyricPosition == ActiveLyricPositionUpperMiddle {
|
||||
ht = size.Height / 3
|
||||
}
|
||||
|
||||
var topSpaceHeight, bottomSpaceHeight float32
|
||||
if l.synced {
|
||||
topSpaceHeight = ht + l.prototypeLyricLineSize.Height/2
|
||||
// end spacer only needs to be big enough - can't be too big
|
||||
// so use a very simple height calculation
|
||||
bottomSpaceHeight = size.Height
|
||||
}
|
||||
l.vbox.Objects[0].(*vSpace).Height = topSpaceHeight
|
||||
l.vbox.Objects[len(l.vbox.Objects)-1].(*vSpace).Height = bottomSpaceHeight
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) updateContent() {
|
||||
if l.vbox == nil {
|
||||
return // renderer not created yet
|
||||
}
|
||||
l.checkStopAnimation()
|
||||
|
||||
lnObj := len(l.vbox.Objects)
|
||||
if lnObj == 0 {
|
||||
l.vbox.Objects = append(l.vbox.Objects, NewVSpace(0), NewVSpace(0))
|
||||
lnObj = 2
|
||||
}
|
||||
l.updateSpacerSize(l.Size())
|
||||
endSpacer := l.vbox.Objects[lnObj-1]
|
||||
for i, line := range l.lines {
|
||||
lineNum := i + 1 // one-indexed
|
||||
useActiveColor := !l.synced || l.currentLine == lineNum
|
||||
if lineNum < lnObj-1 {
|
||||
rt := l.vbox.Objects[lineNum].(*lyricLine)
|
||||
if useActiveColor {
|
||||
l.setLineColor(rt, l.activeLyricColor(), false)
|
||||
} else {
|
||||
l.setLineColor(rt, l.inactiveLyricColor(), false)
|
||||
}
|
||||
l.setLineTextAndProperties(rt, line, lineNum, true)
|
||||
} else if lineNum < lnObj {
|
||||
// replacing end spacer (last element in Objects) with a new richtext
|
||||
l.vbox.Objects[lineNum] = l.newLyricLine(line, lineNum, useActiveColor)
|
||||
} else {
|
||||
// extending the Objects slice
|
||||
l.vbox.Objects = append(l.vbox.Objects, l.newLyricLine(line, lineNum, useActiveColor))
|
||||
}
|
||||
}
|
||||
for i := len(l.lines) + 1; i < lnObj; i++ {
|
||||
l.vbox.Objects[i] = nil
|
||||
}
|
||||
l.vbox.Objects = l.vbox.Objects[:len(l.lines)+1]
|
||||
l.vbox.Objects = append(l.vbox.Objects, endSpacer)
|
||||
l.vbox.Refresh()
|
||||
l.scroll.Offset.Y = l.offsetForLine(l.currentLine)
|
||||
l.scroll.Refresh()
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) setupScrollAnimation(currentLine, nextLine *lyricLine) {
|
||||
// calculate total scroll distance for the animation
|
||||
scrollDist := theme.Padding()
|
||||
if currentLine != nil {
|
||||
scrollDist += currentLine.Size().Height / 2
|
||||
} else {
|
||||
scrollDist += l.prototypeLyricLineSize.Height / 2
|
||||
}
|
||||
if nextLine != nil {
|
||||
scrollDist += nextLine.Size().Height / 2
|
||||
} else {
|
||||
scrollDist += l.prototypeLyricLineSize.Height / 2
|
||||
}
|
||||
|
||||
l.animStartOffset = l.scroll.Offset.Y
|
||||
var alreadyUpdated bool
|
||||
l.anim = fyne.NewAnimation(140*time.Millisecond, func(f float32) {
|
||||
l.scroll.Offset.Y = l.animStartOffset + f*scrollDist
|
||||
l.scroll.Refresh()
|
||||
if !alreadyUpdated && f >= 0.5 {
|
||||
if nextLine != nil {
|
||||
l.setLineColor(nextLine, l.activeLyricColor(), true)
|
||||
}
|
||||
if currentLine != nil {
|
||||
l.setLineColor(currentLine, l.inactiveLyricColor(), true)
|
||||
}
|
||||
alreadyUpdated = true
|
||||
}
|
||||
if f == 1 /*end of animation*/ {
|
||||
l.anim = nil
|
||||
}
|
||||
})
|
||||
l.anim.Curve = fyne.AnimationEaseInOut
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) offsetForLine(lineNum int /*one-indexed*/) float32 {
|
||||
if lineNum == 0 {
|
||||
return 0
|
||||
}
|
||||
pad := theme.Padding()
|
||||
offset := pad + l.prototypeLyricLineSize.Height/2
|
||||
for i := 1; i <= lineNum; i++ {
|
||||
if i > 1 {
|
||||
offset += l.vbox.Objects[i-1].MinSize().Height/2 + pad
|
||||
}
|
||||
offset += l.vbox.Objects[i].MinSize().Height / 2
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) newLyricLine(text string, lineNum int, useActiveColor bool) *lyricLine {
|
||||
ll := newLyricLine(text, nil)
|
||||
l.setLineTextAndProperties(ll, text, lineNum, false)
|
||||
ll.HoveredColorName = l.hoveredLyricColor()
|
||||
if useActiveColor {
|
||||
ll.ColorName = l.activeLyricColor()
|
||||
} else {
|
||||
ll.ColorName = l.inactiveLyricColor()
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) setLineTextAndProperties(ll *lyricLine, text string, lineNum int, refresh bool) {
|
||||
ll.Text = text
|
||||
ll.SizeName = l.textSizeName()
|
||||
ll.Alignment = l.Alignment
|
||||
ll.Tappable = l.synced && l.OnLyricTapped != nil
|
||||
ll.onTapped = func() {
|
||||
if l.OnLyricTapped != nil {
|
||||
l.OnLyricTapped(lineNum)
|
||||
}
|
||||
}
|
||||
if refresh {
|
||||
ll.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) setLineColor(ll *lyricLine, colorName fyne.ThemeColorName, refresh bool) {
|
||||
ll.ColorName = colorName
|
||||
ll.HoveredColorName = l.hoveredLyricColor()
|
||||
if refresh {
|
||||
ll.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) activeLyricColor() fyne.ThemeColorName {
|
||||
if l.ActiveLyricColorName != "" {
|
||||
return l.ActiveLyricColorName
|
||||
}
|
||||
return theme.ColorNameForeground
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) inactiveLyricColor() fyne.ThemeColorName {
|
||||
if l.InactiveLyricColorName != "" {
|
||||
return l.InactiveLyricColorName
|
||||
}
|
||||
return theme.ColorNameDisabled
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) hoveredLyricColor() fyne.ThemeColorName {
|
||||
if l.HoveredLyricColorName != "" {
|
||||
return l.HoveredLyricColorName
|
||||
}
|
||||
return theme.ColorNameHyperlink
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) textSizeName() fyne.ThemeSizeName {
|
||||
if l.TextSizeName != "" {
|
||||
return l.TextSizeName
|
||||
}
|
||||
return theme.SizeNameSubHeadingText
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) checkStopAnimation() bool {
|
||||
if l.anim != nil {
|
||||
l.anim.Stop()
|
||||
l.anim = nil
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) CreateRenderer() fyne.WidgetRenderer {
|
||||
l.vbox = container.NewVBox()
|
||||
l.scroll = container.NewScroll(l.vbox)
|
||||
if l.synced {
|
||||
l.scroll.Direction = container.ScrollNone
|
||||
} else {
|
||||
l.scroll.Direction = container.ScrollVerticalOnly
|
||||
}
|
||||
l.updateContent()
|
||||
return widget.NewSimpleRenderer(l.scroll)
|
||||
}
|
||||
|
||||
type vSpace struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Height float32
|
||||
}
|
||||
|
||||
func NewVSpace(height float32) *vSpace {
|
||||
v := &vSpace{Height: height}
|
||||
v.ExtendBaseWidget(v)
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *vSpace) MinSize() fyne.Size {
|
||||
return fyne.NewSize(0, v.Height)
|
||||
}
|
||||
|
||||
func (v *vSpace) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(layout.NewSpacer())
|
||||
}
|
||||
3
gui/component/lyrics/readme.txt
Normal file
3
gui/component/lyrics/readme.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
license under bsd-3-clause
|
||||
|
||||
https://github.com/supersonic-app/fyne-lyrics
|
||||
44
gui/gctx/context.go
Normal file
44
gui/gctx/context.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package gctx
|
||||
|
||||
import (
|
||||
_logger "AynaLivePlayer/pkg/logger"
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
// gui context
|
||||
|
||||
const (
|
||||
EventChannel = "gui"
|
||||
)
|
||||
|
||||
var Logger _logger.ILogger = nil
|
||||
var Context *GuiContext = nil
|
||||
|
||||
type GuiContext struct {
|
||||
App fyne.App // application
|
||||
Window fyne.Window // main window
|
||||
EventChannel string
|
||||
onMainWindowClosing []func()
|
||||
}
|
||||
|
||||
func NewGuiContext(app fyne.App, mainWindow fyne.Window) *GuiContext {
|
||||
return &GuiContext{
|
||||
App: app,
|
||||
Window: mainWindow,
|
||||
EventChannel: EventChannel,
|
||||
onMainWindowClosing: make([]func(), 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GuiContext) Init() {
|
||||
c.Window.SetOnClosed(func() {
|
||||
for idx, f := range c.onMainWindowClosing {
|
||||
Logger.Debugf("runing gui closing handler #%d", idx)
|
||||
f()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GuiContext) OnMainWindowClosing(f func()) {
|
||||
c.onMainWindowClosing = append(c.onMainWindowClosing, f)
|
||||
}
|
||||
78
gui/gui.go
78
gui/gui.go
@@ -3,7 +3,16 @@ package gui
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
configView "AynaLivePlayer/gui/views/config"
|
||||
"AynaLivePlayer/gui/views/history"
|
||||
"AynaLivePlayer/gui/views/liverooms"
|
||||
"AynaLivePlayer/gui/views/player"
|
||||
"AynaLivePlayer/gui/views/playlists"
|
||||
"AynaLivePlayer/gui/views/search"
|
||||
"AynaLivePlayer/gui/views/systray"
|
||||
"AynaLivePlayer/gui/views/updater"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -18,11 +27,6 @@ import (
|
||||
_logger "AynaLivePlayer/pkg/logger"
|
||||
)
|
||||
|
||||
var App fyne.App
|
||||
var MainWindow fyne.Window
|
||||
var playerWindow fyne.Window
|
||||
var playerWindowHandle uintptr
|
||||
|
||||
var logger _logger.ILogger = nil
|
||||
|
||||
func black_magic() {
|
||||
@@ -31,48 +35,48 @@ func black_magic() {
|
||||
|
||||
func Initialize() {
|
||||
logger = global.Logger.WithPrefix("GUI")
|
||||
gctx.Logger = logger
|
||||
black_magic()
|
||||
logger.Info("Initializing GUI")
|
||||
|
||||
if config.General.CustomFonts != "" {
|
||||
_ = os.Setenv("FYNE_FONT", config.GetAssetPath(config.General.CustomFonts))
|
||||
}
|
||||
App = app.NewWithID(config.ProgramName)
|
||||
//App.Settings().SetTheme(&myTheme{})
|
||||
MainWindow = App.NewWindow(getAppTitle())
|
||||
|
||||
mainApp := app.NewWithID(config.ProgramName)
|
||||
MainWindow := mainApp.NewWindow(getAppTitle())
|
||||
|
||||
gctx.Context = gctx.NewGuiContext(mainApp, MainWindow)
|
||||
gctx.Context.Init()
|
||||
|
||||
gctx.Context.OnMainWindowClosing(func() {
|
||||
_ = config.SaveToConfigFile(config.ConfigPath)
|
||||
logger.Infof("config saved to %s", config.ConfigPath)
|
||||
})
|
||||
|
||||
updater.CreateUpdaterPopUp()
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem(i18n.T("gui.tab.player"),
|
||||
container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.search"),
|
||||
container.NewBorder(createSearchBar(), nil, nil, nil, createSearchList()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.room"),
|
||||
container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.playlist"),
|
||||
container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.history"),
|
||||
container.NewBorder(nil, nil, nil, nil, createHistoryList()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.config"),
|
||||
createConfigLayout(),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.player"), player.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.search"), search.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.room"), liverooms.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.playlist"), playlists.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.history"), history.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.config"), configView.CreateView()),
|
||||
)
|
||||
|
||||
tabs.SetTabLocation(container.TabLocationTop)
|
||||
MainWindow.SetIcon(resource.ImageIcon)
|
||||
MainWindow.SetContent(tabs)
|
||||
//MainWindow.Resize(fyne.NewSize(1280, 720))
|
||||
MainWindow.Resize(fyne.NewSize(config.General.Width, config.General.Height))
|
||||
MainWindow.SetFixedSize(config.General.FixedSize)
|
||||
|
||||
// todo: fix, window were created even if not show. this block gui from closing
|
||||
// i can't create sub window before the main window shows.
|
||||
// setupPlayerWindow()
|
||||
|
||||
// register error
|
||||
global.EventBus.Subscribe("",
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.ErrorUpdate, "gui.show_error", gutil.ThreadSafeHandler(func(e *eventbus.Event) {
|
||||
err := e.Data.(events.ErrorUpdateEvent).Error
|
||||
logger.Warnf("gui received error event: %v, %v", err, err == nil)
|
||||
@@ -82,23 +86,7 @@ func Initialize() {
|
||||
dialog.ShowError(err, MainWindow)
|
||||
}))
|
||||
|
||||
checkUpdate()
|
||||
MainWindow.SetFixedSize(config.General.FixedSize)
|
||||
if config.General.ShowSystemTray {
|
||||
setupSysTray()
|
||||
} else {
|
||||
MainWindow.SetCloseIntercept(
|
||||
func() {
|
||||
// todo: save twice i don;t care
|
||||
_ = config.SaveToConfigFile(config.ConfigPath)
|
||||
MainWindow.Close()
|
||||
})
|
||||
systray.SetupSysTray()
|
||||
}
|
||||
MainWindow.SetOnClosed(func() {
|
||||
logger.Infof("GUI closing")
|
||||
if playerWindow != nil {
|
||||
logger.Infof("player window closing")
|
||||
playerWindow.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build darwin || windows || linux
|
||||
|
||||
package xfyne
|
||||
package gutil
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
@@ -1,9 +1,14 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package xfyne
|
||||
package gutil
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
func GetWindowHandle(window fyne.Window) uintptr {
|
||||
// macos doesn't support --wid. :(
|
||||
return 0
|
||||
glfwWindow := getGlfwWindow(window)
|
||||
if glfwWindow == nil {
|
||||
return 0
|
||||
@@ -1,7 +1,6 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package xfyne
|
||||
package gutil
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
@@ -1,7 +1,6 @@
|
||||
//go:build !darwin && !windows && !linux
|
||||
// +build !darwin,!windows,!linux
|
||||
|
||||
package xfyne
|
||||
package gutil
|
||||
|
||||
import "fyne.io/fyne/v2"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package xfyne
|
||||
package gutil
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
250
gui/liverooms.go
250
gui/liverooms.go
@@ -1,250 +0,0 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var RoomTab = &struct {
|
||||
Rooms *widget.List
|
||||
Index int
|
||||
AddBtn *widget.Button
|
||||
RemoveBtn *widget.Button
|
||||
RoomTitle *widget.Label
|
||||
RoomID *widget.Label
|
||||
Status *widget.Label
|
||||
AutoConnect *widget.Check
|
||||
ConnectBtn *widget.Button
|
||||
DisConnectBtn *widget.Button
|
||||
providers []model.LiveRoomProviderInfo
|
||||
rooms []model.LiveRoom
|
||||
lock sync.RWMutex
|
||||
}{}
|
||||
|
||||
func createRoomSelector() fyne.CanvasObject {
|
||||
RoomTab.Rooms = widget.NewList(
|
||||
func() int {
|
||||
return len(RoomTab.rooms)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return widget.NewLabel("AAAAAAAAAAAAAAAA")
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
object.(*widget.Label).SetText(
|
||||
RoomTab.rooms[id].DisplayName())
|
||||
})
|
||||
RoomTab.AddBtn = widget.NewButton(i18n.T("gui.room.button.add"), func() {
|
||||
providerNames := make([]string, len(RoomTab.providers))
|
||||
for i := 0; i < len(RoomTab.providers); i++ {
|
||||
providerNames[i] = RoomTab.providers[i].Name
|
||||
}
|
||||
descriptionLabel := widget.NewLabel(i18n.T("gui.room.add.prompt"))
|
||||
clientNameEntry := widget.NewSelect(providerNames, func(s string) {
|
||||
for i := 0; i < len(RoomTab.providers); i++ {
|
||||
if RoomTab.providers[i].Name == s {
|
||||
descriptionLabel.SetText(i18n.T(RoomTab.providers[i].Description))
|
||||
break
|
||||
}
|
||||
descriptionLabel.SetText("")
|
||||
}
|
||||
})
|
||||
idEntry := xfyne.EntryDisableUndoRedo(widget.NewEntry())
|
||||
nameEntry := xfyne.EntryDisableUndoRedo(widget.NewEntry())
|
||||
dia := dialog.NewCustomConfirm(
|
||||
i18n.T("gui.room.add.title"),
|
||||
i18n.T("gui.room.add.confirm"),
|
||||
i18n.T("gui.room.add.cancel"),
|
||||
container.NewVBox(
|
||||
container.New(
|
||||
layout.NewFormLayout(),
|
||||
widget.NewLabel(i18n.T("gui.room.add.name")),
|
||||
nameEntry,
|
||||
widget.NewLabel(i18n.T("gui.room.add.client_name")),
|
||||
clientNameEntry,
|
||||
widget.NewLabel(i18n.T("gui.room.add.id_url")),
|
||||
idEntry,
|
||||
),
|
||||
descriptionLabel,
|
||||
),
|
||||
func(b bool) {
|
||||
if b && len(clientNameEntry.Selected) > 0 && len(idEntry.Text) > 0 {
|
||||
logger.Infof("Add room %s %s", clientNameEntry.Selected, idEntry.Text)
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomAddCmd,
|
||||
events.LiveRoomAddCmdEvent{
|
||||
Title: nameEntry.Text,
|
||||
Provider: clientNameEntry.Selected,
|
||||
RoomKey: idEntry.Text,
|
||||
})
|
||||
}
|
||||
},
|
||||
MainWindow,
|
||||
)
|
||||
dia.Resize(fyne.NewSize(512, 256))
|
||||
dia.Show()
|
||||
})
|
||||
RoomTab.RemoveBtn = widget.NewButton(i18n.T("gui.room.button.remove"), func() {
|
||||
if len(RoomTab.rooms) == 0 {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomRemoveCmd,
|
||||
events.LiveRoomRemoveCmdEvent{
|
||||
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
|
||||
})
|
||||
})
|
||||
RoomTab.Rooms.OnSelected = func(id widget.ListItemID) {
|
||||
if id >= len(RoomTab.rooms) {
|
||||
return
|
||||
}
|
||||
logger.Infof("Select room %s", RoomTab.rooms[id].LiveRoom.Identifier())
|
||||
RoomTab.Index = id
|
||||
room := RoomTab.rooms[RoomTab.Index]
|
||||
RoomTab.RoomTitle.SetText(room.DisplayName())
|
||||
RoomTab.RoomID.SetText(room.LiveRoom.Identifier())
|
||||
RoomTab.AutoConnect.SetChecked(room.Config.AutoConnect)
|
||||
if room.Status {
|
||||
RoomTab.Status.SetText(i18n.T("gui.room.status.connected"))
|
||||
} else {
|
||||
RoomTab.Status.SetText(i18n.T("gui.room.status.disconnected"))
|
||||
}
|
||||
RoomTab.Status.Refresh()
|
||||
}
|
||||
registerRoomHandlers()
|
||||
return container.NewHBox(
|
||||
container.NewBorder(
|
||||
nil, container.NewCenter(container.NewHBox(RoomTab.AddBtn, RoomTab.RemoveBtn)),
|
||||
nil, nil,
|
||||
RoomTab.Rooms,
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
)
|
||||
}
|
||||
|
||||
func registerRoomHandlers() {
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomProviderUpdate,
|
||||
"gui.liveroom.provider_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
RoomTab.providers = event.Data.(events.LiveRoomProviderUpdateEvent).Providers
|
||||
RoomTab.Rooms.Refresh()
|
||||
}))
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomRoomsUpdate,
|
||||
"gui.liveroom.rooms_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
logger.Infof("Update rooms")
|
||||
data := event.Data.(events.LiveRoomRoomsUpdateEvent)
|
||||
RoomTab.lock.Lock()
|
||||
RoomTab.rooms = data.Rooms
|
||||
RoomTab.Rooms.Select(0)
|
||||
RoomTab.Rooms.Refresh()
|
||||
RoomTab.lock.Unlock()
|
||||
}))
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomStatusUpdate,
|
||||
"gui.liveroom.room_status_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
room := event.Data.(events.LiveRoomStatusUpdateEvent).Room
|
||||
index := -1
|
||||
for i := 0; i < len(RoomTab.rooms); i++ {
|
||||
if RoomTab.rooms[i].LiveRoom.Identifier() == room.LiveRoom.Identifier() {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return
|
||||
}
|
||||
RoomTab.rooms[index] = room
|
||||
// add lock to avoid race condition
|
||||
RoomTab.lock.Lock()
|
||||
RoomTab.Rooms.Refresh()
|
||||
RoomTab.lock.Unlock()
|
||||
if index == RoomTab.Index {
|
||||
RoomTab.RoomTitle.SetText(room.DisplayName())
|
||||
RoomTab.RoomID.SetText(room.LiveRoom.Identifier())
|
||||
RoomTab.AutoConnect.SetChecked(room.Config.AutoConnect)
|
||||
if room.Status {
|
||||
RoomTab.Status.SetText(i18n.T("gui.room.status.connected"))
|
||||
} else {
|
||||
RoomTab.Status.SetText(i18n.T("gui.room.status.disconnected"))
|
||||
}
|
||||
RoomTab.Status.Refresh()
|
||||
}
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
func createRoomController() fyne.CanvasObject {
|
||||
RoomTab.ConnectBtn = widget.NewButton(i18n.T("gui.room.btn.connect"), func() {
|
||||
if RoomTab.Index >= len(RoomTab.rooms) {
|
||||
return
|
||||
}
|
||||
RoomTab.ConnectBtn.Disable()
|
||||
logger.Infof("Connect to room %s", RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier())
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomOperationCmd,
|
||||
events.LiveRoomOperationCmdEvent{
|
||||
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
|
||||
SetConnect: true,
|
||||
})
|
||||
})
|
||||
RoomTab.DisConnectBtn = widget.NewButton(i18n.T("gui.room.btn.disconnect"), func() {
|
||||
if RoomTab.Index >= len(RoomTab.rooms) {
|
||||
return
|
||||
}
|
||||
RoomTab.DisConnectBtn.Disable()
|
||||
logger.Infof("Disconnect from room %s", RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier())
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomOperationCmd,
|
||||
events.LiveRoomOperationCmdEvent{
|
||||
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
|
||||
SetConnect: false,
|
||||
})
|
||||
})
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomOperationFinish,
|
||||
"gui.liveroom.operation_finish",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
RoomTab.ConnectBtn.Enable()
|
||||
RoomTab.DisConnectBtn.Enable()
|
||||
}))
|
||||
RoomTab.Status = widget.NewLabel(i18n.T("gui.room.waiting"))
|
||||
RoomTab.RoomTitle = widget.NewLabel("")
|
||||
RoomTab.RoomID = widget.NewLabel("")
|
||||
RoomTab.AutoConnect = widget.NewCheck(i18n.T("gui.room.check.autoconnect"), func(b bool) {
|
||||
if RoomTab.Index >= len(RoomTab.rooms) {
|
||||
return
|
||||
}
|
||||
logger.Infof("Change room %s autoconnect to %v", RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(), b)
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomConfigChangeCmd,
|
||||
events.LiveRoomConfigChangeCmdEvent{
|
||||
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
|
||||
Config: model.LiveRoomConfig{
|
||||
AutoConnect: b,
|
||||
},
|
||||
})
|
||||
return
|
||||
})
|
||||
RoomTab.Rooms.Select(0)
|
||||
return container.NewVBox(
|
||||
RoomTab.RoomTitle,
|
||||
RoomTab.RoomID,
|
||||
RoomTab.Status,
|
||||
container.NewHBox(widget.NewLabel(i18n.T("gui.room.check.autoconnect")), RoomTab.AutoConnect),
|
||||
container.NewHBox(RoomTab.ConnectBtn, RoomTab.DisConnectBtn),
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
func createLyricObj(lyric *miaosic.Lyrics) []fyne.CanvasObject {
|
||||
lrcs := make([]fyne.CanvasObject, len(lyric.Content))
|
||||
for i := 0; i < len(lrcs); i++ {
|
||||
lr := widget.NewLabelWithStyle(
|
||||
lyric.Content[i].Lyric,
|
||||
fyne.TextAlignCenter, fyne.TextStyle{Italic: true})
|
||||
//lr.Wrapping = fyne.TextWrapWord
|
||||
// todo fix fyne bug
|
||||
lr.Wrapping = fyne.TextWrapBreak
|
||||
lrcs[i] = lr
|
||||
}
|
||||
return lrcs
|
||||
}
|
||||
|
||||
func createLyricWindow() fyne.Window {
|
||||
// create widgets
|
||||
w := App.NewWindow(i18n.T("gui.lyric.title"))
|
||||
currentLrc := newLabelWithWrapping("", fyne.TextWrapBreak)
|
||||
currentLrc.Alignment = fyne.TextAlignCenter
|
||||
fullLrc := container.NewVBox()
|
||||
lrcWindow := container.NewVScroll(fullLrc)
|
||||
prevIndex := 0
|
||||
w.SetContent(container.NewBorder(nil,
|
||||
container.NewVBox(widget.NewSeparator(), currentLrc),
|
||||
nil, nil,
|
||||
lrcWindow))
|
||||
w.Resize(fyne.NewSize(360, 540))
|
||||
w.CenterOnScreen()
|
||||
|
||||
// register handlers
|
||||
global.EventBus.Subscribe("",
|
||||
events.PlayerLyricPosUpdate, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
e := event.Data.(events.PlayerLyricPosUpdateEvent)
|
||||
logger.Debug("lyric update", e)
|
||||
if prevIndex >= len(fullLrc.Objects) || e.CurrentIndex >= len(fullLrc.Objects) {
|
||||
// fix race condition
|
||||
return
|
||||
}
|
||||
if e.CurrentIndex == -1 {
|
||||
currentLrc.SetText("")
|
||||
return
|
||||
}
|
||||
fullLrc.Objects[prevIndex].(*widget.Label).TextStyle.Bold = false
|
||||
fullLrc.Objects[prevIndex].Refresh()
|
||||
fullLrc.Objects[e.CurrentIndex].(*widget.Label).TextStyle.Bold = true
|
||||
fullLrc.Objects[e.CurrentIndex].Refresh()
|
||||
prevIndex = e.CurrentIndex
|
||||
currentLrc.SetText(e.CurrentLine.Lyric)
|
||||
lrcWindow.Scrolled(&fyne.ScrollEvent{
|
||||
Scrolled: fyne.Delta{
|
||||
DX: 0,
|
||||
DY: lrcWindow.Offset.Y - float32(e.CurrentIndex-2)/float32(e.Total)*lrcWindow.Content.Size().Height,
|
||||
},
|
||||
})
|
||||
fullLrc.Refresh()
|
||||
}))
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerLyricReload, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
e := event.Data.(events.PlayerLyricReloadEvent)
|
||||
fullLrc.Objects = createLyricObj(&e.Lyrics)
|
||||
lrcWindow.Refresh()
|
||||
}))
|
||||
|
||||
_ = global.EventBus.Publish(events.PlayerLyricRequestCmd, events.PlayerLyricRequestCmdEvent{})
|
||||
|
||||
w.SetOnClosed(func() {
|
||||
global.EventBus.Unsubscribe(events.PlayerLyricReload, "player.lyric.current_lyric")
|
||||
PlayController.LrcWindowOpen = false
|
||||
})
|
||||
return w
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package gui
|
||||
package config
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
@@ -37,13 +38,13 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
|
||||
if b {
|
||||
mode = model.PlaylistModeRandom
|
||||
}
|
||||
logger.Infof("Set player playlist mode to %d", mode)
|
||||
_ = global.EventBus.Publish(events.PlaylistModeChangeCmd(model.PlaylistIDPlayer),
|
||||
gctx.Logger.Infof("Set player playlist mode to %d", mode)
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistModeChangeCmd(model.PlaylistIDPlayer),
|
||||
events.PlaylistModeChangeCmdEvent{
|
||||
Mode: mode,
|
||||
})
|
||||
})
|
||||
global.EventBus.Subscribe("", events.PlaylistModeChangeUpdate(model.PlaylistIDPlayer),
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlaylistModeChangeUpdate(model.PlaylistIDPlayer),
|
||||
"gui.config.basic.random_playlist.player",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlaylistModeChangeUpdateEvent)
|
||||
@@ -56,13 +57,13 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
|
||||
if b {
|
||||
mode = model.PlaylistModeRandom
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlaylistModeChangeCmd(model.PlaylistIDSystem),
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistModeChangeCmd(model.PlaylistIDSystem),
|
||||
events.PlaylistModeChangeCmdEvent{
|
||||
Mode: mode,
|
||||
})
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("", events.PlaylistModeChangeUpdate(model.PlaylistIDSystem),
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlaylistModeChangeUpdate(model.PlaylistIDSystem),
|
||||
"gui.config.basic.random_playlist.system",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlaylistModeChangeUpdateEvent)
|
||||
@@ -80,11 +81,11 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{
|
||||
Device: name,
|
||||
})
|
||||
})
|
||||
global.EventBus.Subscribe("",
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.PlayerAudioDeviceUpdate,
|
||||
"gui.config.basic.audio_device.update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
@@ -99,7 +100,7 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
|
||||
currentDevice = device.Description
|
||||
}
|
||||
}
|
||||
logger.Infof("update audio device. set current to %s (%s)", data.Current, deviceDesc2Name[data.Current])
|
||||
gctx.Logger.Infof("update audio device. set current to %s (%s)", data.Current, deviceDesc2Name[data.Current])
|
||||
deviceSel.Options = devices
|
||||
deviceSel.Selected = currentDevice
|
||||
deviceSel.Refresh()
|
||||
@@ -123,7 +124,7 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
|
||||
config.General.AutoCheckUpdate),
|
||||
)
|
||||
checkUpdateBtn := widget.NewButton(i18n.T("gui.config.basic.check_update"), func() {
|
||||
_ = global.EventBus.Publish(events.CheckUpdateCmd, events.CheckUpdateCmdEvent{})
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.CheckUpdateCmd, events.CheckUpdateCmdEvent{})
|
||||
})
|
||||
useSysPlaylistBtn := container.NewHBox(
|
||||
widget.NewLabel(i18n.T("gui.config.basic.use_system_playlist")),
|
||||
@@ -1,4 +1,4 @@
|
||||
package gui
|
||||
package config
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/gui/component"
|
||||
@@ -21,7 +21,7 @@ func AddConfigLayout(cfgs ...ConfigLayout) {
|
||||
ConfigList = append(ConfigList, cfgs...)
|
||||
}
|
||||
|
||||
func createConfigLayout() fyne.CanvasObject {
|
||||
func CreateView() fyne.CanvasObject {
|
||||
// initialize config panels
|
||||
for _, c := range ConfigList {
|
||||
c.CreatePanel()
|
||||
@@ -1,9 +1,11 @@
|
||||
package gui
|
||||
package history
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -15,16 +17,34 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var History = &struct {
|
||||
Medias []model.Media
|
||||
List *widget.List
|
||||
mux sync.RWMutex
|
||||
}{}
|
||||
var medias []model.Media
|
||||
var listWidget *widget.List
|
||||
var mux sync.RWMutex
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
view := createHistoryList()
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.PlayerPlayingUpdate,
|
||||
"gui.history.playing_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
|
||||
return
|
||||
}
|
||||
mux.Lock()
|
||||
medias = append(medias, event.Data.(events.PlayerPlayingUpdateEvent).Media)
|
||||
if len(medias) > 1000 {
|
||||
medias = medias[len(medias)-1000:]
|
||||
}
|
||||
listWidget.Refresh()
|
||||
mux.Unlock()
|
||||
}))
|
||||
return view
|
||||
}
|
||||
|
||||
func createHistoryList() fyne.CanvasObject {
|
||||
History.List = widget.NewList(
|
||||
listWidget = widget.NewList(
|
||||
func() int {
|
||||
return len(History.Medias)
|
||||
return len(medias)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return container.NewBorder(nil, nil,
|
||||
@@ -34,12 +54,12 @@ func createHistoryList() fyne.CanvasObject {
|
||||
widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil),
|
||||
),
|
||||
container.NewGridWithColumns(3,
|
||||
newLabelWithWrapping("title", fyne.TextTruncate),
|
||||
newLabelWithWrapping("artist", fyne.TextTruncate),
|
||||
newLabelWithWrapping("user", fyne.TextTruncate)))
|
||||
component.NewLabelWithOpts("title", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("artist", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("user", component.LabelTruncation(fyne.TextTruncateClip))))
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
m := History.Medias[len(History.Medias)-id-1]
|
||||
m := medias[len(medias)-id-1]
|
||||
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
|
||||
m.Info.Title)
|
||||
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
|
||||
@@ -50,18 +70,17 @@ func createHistoryList() fyne.CanvasObject {
|
||||
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
|
||||
m.User = model.SystemUser
|
||||
btns[0].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
|
||||
Media: m,
|
||||
})
|
||||
}
|
||||
btns[1].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
|
||||
Media: m,
|
||||
Position: -1,
|
||||
})
|
||||
}
|
||||
})
|
||||
registerHistoryHandler()
|
||||
return container.NewBorder(
|
||||
container.NewBorder(nil, nil,
|
||||
widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.history.operation")),
|
||||
@@ -70,17 +89,6 @@ func createHistoryList() fyne.CanvasObject {
|
||||
widget.NewLabel(i18n.T("gui.history.artist")),
|
||||
widget.NewLabel(i18n.T("gui.history.user")))),
|
||||
nil, nil, nil,
|
||||
History.List,
|
||||
listWidget,
|
||||
)
|
||||
}
|
||||
|
||||
func registerHistoryHandler() {
|
||||
global.EventBus.Subscribe("",
|
||||
events.PlaylistDetailUpdate(model.PlaylistIDHistory),
|
||||
"gui.history.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
History.mux.Lock()
|
||||
History.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias
|
||||
History.List.Refresh()
|
||||
History.mux.Unlock()
|
||||
}))
|
||||
}
|
||||
195
gui/views/liverooms/liverooms.go
Normal file
195
gui/views/liverooms/liverooms.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package liverooms
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
view := container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController())
|
||||
registerRoomHandlers()
|
||||
return view
|
||||
}
|
||||
|
||||
var providers []model.LiveRoomProviderInfo = make([]model.LiveRoomProviderInfo, 0)
|
||||
var rooms []model.LiveRoom = make([]model.LiveRoom, 0)
|
||||
|
||||
var lock sync.RWMutex
|
||||
|
||||
var currentRoomView = &struct {
|
||||
roomTitle *widget.Label
|
||||
roomID *widget.Label
|
||||
status *widget.Label
|
||||
autoConnect *widget.Check
|
||||
connectBtn *widget.Button
|
||||
disConnectBtn *widget.Button
|
||||
}{}
|
||||
|
||||
var currentIndex int = 0
|
||||
|
||||
func getCurrentRoom() (model.LiveRoom, bool) {
|
||||
lock.RLock()
|
||||
if currentIndex >= len(rooms) {
|
||||
lock.RUnlock()
|
||||
return model.LiveRoom{}, false
|
||||
}
|
||||
room := rooms[currentIndex]
|
||||
lock.RUnlock()
|
||||
return room, true
|
||||
}
|
||||
|
||||
func renderCurrentRoom() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
currentRoomView.roomTitle.SetText("")
|
||||
currentRoomView.roomID.SetText("")
|
||||
currentRoomView.autoConnect.SetChecked(false)
|
||||
currentRoomView.status.SetText(i18n.T("gui.room.waiting"))
|
||||
return
|
||||
}
|
||||
currentRoomView.roomTitle.SetText(room.DisplayName())
|
||||
currentRoomView.roomID.SetText(room.LiveRoom.Identifier())
|
||||
currentRoomView.autoConnect.SetChecked(room.Config.AutoConnect)
|
||||
if room.Status {
|
||||
currentRoomView.status.SetText(i18n.T("gui.room.status.connected"))
|
||||
} else {
|
||||
currentRoomView.status.SetText(i18n.T("gui.room.status.disconnected"))
|
||||
}
|
||||
}
|
||||
|
||||
func registerRoomHandlers() {
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.LiveRoomProviderUpdate,
|
||||
"gui.liveroom.provider_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
providers = event.Data.(events.LiveRoomProviderUpdateEvent).Providers
|
||||
//RoomTab.Rooms.Refresh()
|
||||
}))
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.UpdateLiveRoomRooms,
|
||||
"gui.liveroom.rooms_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
gctx.Logger.Infof("Update rooms")
|
||||
lock.Lock()
|
||||
rooms = event.Data.(events.UpdateLiveRoomRoomsData).Rooms
|
||||
lock.Unlock()
|
||||
renderRoomList()
|
||||
renderCurrentRoom()
|
||||
}))
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.UpdateLiveRoomStatus,
|
||||
"gui.liveroom.room_status_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
room := event.Data.(events.UpdateLiveRoomStatusData).Room
|
||||
lock.Lock()
|
||||
index := -1
|
||||
for i := 0; i < len(rooms); i++ {
|
||||
if rooms[i].LiveRoom.Identifier() == room.LiveRoom.Identifier() {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
lock.Unlock()
|
||||
return
|
||||
}
|
||||
rooms[index] = room
|
||||
lock.Unlock()
|
||||
if index == currentIndex {
|
||||
renderCurrentRoom()
|
||||
}
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
func createRoomController() fyne.CanvasObject {
|
||||
currentRoomView.connectBtn = widget.NewButton(i18n.T("gui.room.btn.connect"), func() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("Connect to room %s", room.LiveRoom.Identifier())
|
||||
currentRoomView.connectBtn.Disable()
|
||||
go func() {
|
||||
resp, err := global.EventBus.Call(events.CmdLiveRoomOperation, events.ReplyLiveRoomOperation, events.CmdLiveRoomOperationData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
SetConnect: true,
|
||||
})
|
||||
if err != nil {
|
||||
gctx.Logger.Errorf("failed to connect to room %s", room.LiveRoom.Identifier())
|
||||
gutil.RunInFyneThread(currentRoomView.connectBtn.Enable)
|
||||
return
|
||||
}
|
||||
if resp.Data.(events.ReplyLiveRoomOperationData).Err != nil {
|
||||
err = resp.Data.(events.ReplyLiveRoomOperationData).Err
|
||||
}
|
||||
if err != nil {
|
||||
// todo: show error
|
||||
}
|
||||
gutil.RunInFyneThread(currentRoomView.connectBtn.Enable)
|
||||
}()
|
||||
})
|
||||
currentRoomView.disConnectBtn = widget.NewButton(i18n.T("gui.room.btn.disconnect"), func() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("disconnect to room %s", room.LiveRoom.Identifier())
|
||||
currentRoomView.disConnectBtn.Disable()
|
||||
go func() {
|
||||
resp, err := global.EventBus.Call(events.CmdLiveRoomOperation, events.ReplyLiveRoomOperation, events.CmdLiveRoomOperationData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
SetConnect: false,
|
||||
})
|
||||
if err != nil {
|
||||
gctx.Logger.Errorf("failed to disconnect to room %s", room.LiveRoom.Identifier())
|
||||
gutil.RunInFyneThread(currentRoomView.disConnectBtn.Enable)
|
||||
return
|
||||
}
|
||||
if resp.Data.(events.ReplyLiveRoomOperationData).Err != nil {
|
||||
err = resp.Data.(events.ReplyLiveRoomOperationData).Err
|
||||
}
|
||||
if err != nil {
|
||||
// todo: show error
|
||||
}
|
||||
gutil.RunInFyneThread(currentRoomView.disConnectBtn.Enable)
|
||||
}()
|
||||
})
|
||||
|
||||
currentRoomView.status = widget.NewLabel(i18n.T("gui.room.waiting"))
|
||||
currentRoomView.roomTitle = widget.NewLabel("")
|
||||
currentRoomView.roomID = widget.NewLabel("")
|
||||
|
||||
currentRoomView.autoConnect = widget.NewCheck(i18n.T("gui.room.check.autoconnect"), func(b bool) {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("Change room %s autoconnect to %v", room.LiveRoom.Identifier(), b)
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.CmdLiveRoomConfigChange,
|
||||
events.CmdLiveRoomConfigChangeData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
Config: model.LiveRoomConfig{
|
||||
AutoConnect: b,
|
||||
},
|
||||
})
|
||||
return
|
||||
})
|
||||
return container.NewVBox(
|
||||
currentRoomView.roomTitle,
|
||||
currentRoomView.roomID,
|
||||
currentRoomView.status,
|
||||
container.NewHBox(widget.NewLabel(i18n.T("gui.room.check.autoconnect")), currentRoomView.autoConnect),
|
||||
container.NewHBox(currentRoomView.connectBtn, currentRoomView.disConnectBtn),
|
||||
)
|
||||
}
|
||||
120
gui/views/liverooms/selector.go
Normal file
120
gui/views/liverooms/selector.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package liverooms
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var roomSelectorView = &struct {
|
||||
rooms *widget.List
|
||||
addBtn *widget.Button
|
||||
removeBtn *widget.Button
|
||||
}{}
|
||||
|
||||
func renderRoomList() {
|
||||
lock.Lock()
|
||||
roomSelectorView.rooms.Refresh()
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
func createRoomSelector() fyne.CanvasObject {
|
||||
roomSelectorView.rooms = widget.NewList(
|
||||
func() int {
|
||||
return len(rooms)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return widget.NewLabel("AAAAAAAAAAAAAAAA")
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
object.(*widget.Label).SetText(
|
||||
rooms[id].DisplayName())
|
||||
})
|
||||
roomSelectorView.addBtn = widget.NewButton(i18n.T("gui.room.button.add"), func() {
|
||||
providerNames := make([]string, len(providers))
|
||||
for i := 0; i < len(providers); i++ {
|
||||
providerNames[i] = providers[i].Name
|
||||
}
|
||||
descriptionLabel := widget.NewLabel(i18n.T("gui.room.add.prompt"))
|
||||
clientNameEntry := widget.NewSelect(providerNames, func(s string) {
|
||||
for i := 0; i < len(providers); i++ {
|
||||
if providers[i].Name == s {
|
||||
descriptionLabel.SetText(i18n.T(providers[i].Description))
|
||||
break
|
||||
}
|
||||
descriptionLabel.SetText("")
|
||||
}
|
||||
})
|
||||
idEntry := widget.NewEntry()
|
||||
nameEntry := widget.NewEntry()
|
||||
dia := dialog.NewCustomConfirm(
|
||||
i18n.T("gui.room.add.title"),
|
||||
i18n.T("gui.room.add.confirm"),
|
||||
i18n.T("gui.room.add.cancel"),
|
||||
container.NewVBox(
|
||||
container.New(
|
||||
layout.NewFormLayout(),
|
||||
widget.NewLabel(i18n.T("gui.room.add.name")),
|
||||
nameEntry,
|
||||
widget.NewLabel(i18n.T("gui.room.add.client_name")),
|
||||
clientNameEntry,
|
||||
widget.NewLabel(i18n.T("gui.room.add.id_url")),
|
||||
idEntry,
|
||||
),
|
||||
descriptionLabel,
|
||||
),
|
||||
func(b bool) {
|
||||
if b && len(clientNameEntry.Selected) > 0 && len(idEntry.Text) > 0 {
|
||||
gctx.Logger.Infof("Add room %s %s", clientNameEntry.Selected, idEntry.Text)
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.CmdLiveRoomAdd,
|
||||
events.CmdLiveRoomAddData{
|
||||
Title: nameEntry.Text,
|
||||
Provider: clientNameEntry.Selected,
|
||||
RoomKey: idEntry.Text,
|
||||
})
|
||||
}
|
||||
},
|
||||
gctx.Context.Window,
|
||||
)
|
||||
dia.Resize(fyne.NewSize(512, 256))
|
||||
dia.Show()
|
||||
})
|
||||
roomSelectorView.removeBtn = widget.NewButton(i18n.T("gui.room.button.remove"), func() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.CmdLiveRoomRemove,
|
||||
events.CmdLiveRoomRemoveData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
})
|
||||
})
|
||||
roomSelectorView.rooms.OnSelected = func(id widget.ListItemID) {
|
||||
if id >= len(rooms) {
|
||||
return
|
||||
}
|
||||
currentIndex = id
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("Select room %s", room.LiveRoom.Identifier())
|
||||
renderCurrentRoom()
|
||||
}
|
||||
return container.NewHBox(
|
||||
container.NewBorder(
|
||||
nil, container.NewCenter(container.NewHBox(roomSelectorView.addBtn, roomSelectorView.removeBtn)),
|
||||
nil, nil,
|
||||
roomSelectorView.rooms,
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package gui
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -32,7 +33,7 @@ type PlayControllerContainer struct {
|
||||
ButtonLrc *widget.Button
|
||||
ButtonPlayer *widget.Button
|
||||
LrcWindowOpen bool
|
||||
CurrentTime *widget.Label
|
||||
CurrentTime *component.LabelFixedSize
|
||||
TotalTime *widget.Label
|
||||
}
|
||||
|
||||
@@ -45,30 +46,36 @@ var PlayController = &PlayControllerContainer{}
|
||||
|
||||
func registerPlayControllerHandler() {
|
||||
PlayController.ButtonPrev.OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: 0,
|
||||
Absolute: true,
|
||||
})
|
||||
}
|
||||
PlayController.ButtonSwitch.OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
|
||||
}
|
||||
PlayController.ButtonNext.OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
|
||||
}
|
||||
|
||||
PlayController.ButtonLrc.OnTapped = func() {
|
||||
if !PlayController.LrcWindowOpen {
|
||||
PlayController.LrcWindowOpen = true
|
||||
createLyricWindow().Show()
|
||||
createLyricWindowV2().Show()
|
||||
}
|
||||
}
|
||||
|
||||
gctx.Context.OnMainWindowClosing(func() {
|
||||
if lyricWindow != nil {
|
||||
lyricWindow.Close()
|
||||
}
|
||||
})
|
||||
|
||||
PlayController.ButtonPlayer.OnTapped = func() {
|
||||
showPlayerWindow()
|
||||
}
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
if event.Data.(events.PlayerPropertyPauseUpdateEvent).Paused {
|
||||
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
|
||||
} else {
|
||||
@@ -77,15 +84,15 @@ func registerPlayControllerHandler() {
|
||||
PlayController.ButtonSwitch.Refresh()
|
||||
}))
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", func(event *eventbus.Event) {
|
||||
if PlayController.Progress.Dragging {
|
||||
return
|
||||
}
|
||||
PlayController.Progress.Value = event.Data.(events.PlayerPropertyPercentPosUpdateEvent).PercentPos * 10
|
||||
PlayController.Progress.Refresh()
|
||||
}))
|
||||
gutil.RunInFyneThread(PlayController.Progress.Refresh)
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyStateUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyStateUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
state := event.Data.(events.PlayerPropertyStateUpdateEvent).State
|
||||
if state == model.PlayerStateIdle || state == model.PlayerStateLoading {
|
||||
PlayController.Progress.Value = 0
|
||||
@@ -101,33 +108,33 @@ func registerPlayControllerHandler() {
|
||||
|
||||
PlayController.Progress.Max = 0
|
||||
PlayController.Progress.OnDragEnd = func(f float64) {
|
||||
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: f / 10,
|
||||
Absolute: false,
|
||||
})
|
||||
}
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
PlayController.CurrentTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos)))
|
||||
}))
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
PlayController.TotalTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration)))
|
||||
}))
|
||||
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
PlayController.Volume.Value = event.Data.(events.PlayerPropertyVolumeUpdateEvent).Volume
|
||||
PlayController.Volume.Refresh()
|
||||
}))
|
||||
|
||||
PlayController.Volume.OnChanged = func(f float64) {
|
||||
_ = global.EventBus.Publish(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
|
||||
Volume: f,
|
||||
})
|
||||
}
|
||||
|
||||
// todo: double check cover loading for new thread model
|
||||
global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "gui.player.updateinfo", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPlayingUpdate, "gui.player.updateinfo", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
|
||||
PlayController.Progress.Value = 0
|
||||
PlayController.Progress.Max = 0
|
||||
@@ -163,7 +170,7 @@ func registerPlayControllerHandler() {
|
||||
picture, err := gutil.NewImageFromPlayerPicture(media.Info.Cover)
|
||||
if err != nil {
|
||||
ch <- nil
|
||||
logger.Errorf("fail to load cover: %v", err)
|
||||
gctx.Logger.Errorf("fail to load cover: %v", err)
|
||||
return
|
||||
}
|
||||
ch <- picture
|
||||
@@ -219,15 +226,15 @@ func createPlayControllerV2() fyne.CanvasObject {
|
||||
controls.SeparatorThickness = 0
|
||||
|
||||
PlayController.Progress = component.NewSliderPlus(0, 1000)
|
||||
PlayController.CurrentTime = widget.NewLabel("0:00")
|
||||
PlayController.TotalTime = widget.NewLabel("0:00")
|
||||
PlayController.CurrentTime = component.NewLabelFixedSize(widget.NewLabel("00:00"))
|
||||
PlayController.TotalTime = widget.NewLabel("00:00")
|
||||
progressItem := container.NewBorder(nil, nil,
|
||||
PlayController.CurrentTime,
|
||||
PlayController.TotalTime,
|
||||
PlayController.Progress)
|
||||
|
||||
PlayController.Title = widget.NewLabel("Title")
|
||||
PlayController.Title.Wrapping = fyne.TextTruncate
|
||||
PlayController.Title.Truncation = fyne.TextTruncateClip
|
||||
PlayController.Artist = widget.NewLabel("Artist")
|
||||
PlayController.Username = widget.NewLabel("Username")
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package gui
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
)
|
||||
|
||||
func registerHandlers() {
|
||||
global.EventBus.Subscribe("", events.GUISetPlayerWindowOpenCmd, "gui.player.videoplayer.handleopen", func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.GUISetPlayerWindowOpenCmd, "gui.player.videoplayer.handleopen", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.GUISetPlayerWindowOpenCmdEvent)
|
||||
if data.SetOpen {
|
||||
playerWindow.Close()
|
||||
98
gui/views/player/lyric.go
Normal file
98
gui/views/player/lyric.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component/lyrics"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var lyricWindow fyne.Window = nil
|
||||
var lyricViewer *lyrics.LyricsViewer = nil
|
||||
var currLyrics []string
|
||||
var currentLrcObj miaosic.Lyrics = miaosic.Lyrics{}
|
||||
var lrcmux sync.RWMutex
|
||||
|
||||
func setupLyricViewer() {
|
||||
if lyricWindow != nil {
|
||||
return
|
||||
}
|
||||
lyricViewer = lyrics.NewLyricsViewer()
|
||||
lyricViewer.ActiveLyricPosition = lyrics.ActiveLyricPositionUpperMiddle
|
||||
lyricViewer.Alignment = fyne.TextAlignCenter
|
||||
lyricViewer.HoveredLyricColorName = theme.ColorNameDisabled
|
||||
lyricViewer.SetLyrics([]string{""}, true)
|
||||
lyricViewer.OnLyricTapped = func(lineNum int) {
|
||||
lineNum = lineNum - 1
|
||||
if lineNum < 0 {
|
||||
return
|
||||
}
|
||||
lrcmux.Lock()
|
||||
if lineNum >= len(currentLrcObj.Content) {
|
||||
lrcmux.Unlock()
|
||||
return
|
||||
}
|
||||
line := currentLrcObj.Content[lineNum]
|
||||
lrcmux.Unlock()
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: line.Time,
|
||||
Absolute: true,
|
||||
})
|
||||
}
|
||||
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.UpdateCurrentLyric, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
e := event.Data.(events.UpdateCurrentLyricData)
|
||||
tmpLyric := make([]string, 0)
|
||||
for _, l := range e.Lyrics.Content {
|
||||
tmpLyric = append(tmpLyric, l.Lyric)
|
||||
}
|
||||
// ensure at least one line
|
||||
if len(tmpLyric) == 0 {
|
||||
tmpLyric = append(tmpLyric, "")
|
||||
}
|
||||
lrcmux.Lock()
|
||||
currentLrcObj = event.Data.(events.UpdateCurrentLyricData).Lyrics
|
||||
currLyrics = tmpLyric
|
||||
lyricViewer.SetLyrics(currLyrics, true)
|
||||
lyricViewer.SetCurrentLine(0)
|
||||
lrcmux.Unlock()
|
||||
}))
|
||||
|
||||
// register handlers
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.PlayerLyricPosUpdate, "player.lyric.lyric_pos_update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
e := event.Data.(events.PlayerLyricPosUpdateEvent)
|
||||
gctx.Logger.Debug("lyric update", e)
|
||||
lrcmux.Lock()
|
||||
if e.CurrentIndex >= len(currLyrics) {
|
||||
// fix race condition
|
||||
lrcmux.Unlock()
|
||||
return
|
||||
}
|
||||
index := 0
|
||||
if e.CurrentIndex != -1 {
|
||||
index = e.CurrentIndex
|
||||
}
|
||||
lyricViewer.SetCurrentLine(index + 1)
|
||||
lrcmux.Unlock()
|
||||
}))
|
||||
}
|
||||
|
||||
func createLyricWindowV2() fyne.Window {
|
||||
// create widgets
|
||||
lyricWindow = gctx.Context.App.NewWindow(i18n.T("gui.lyric.title"))
|
||||
lyricWindow.SetContent(lyricViewer)
|
||||
lyricWindow.Resize(fyne.NewSize(360, 540))
|
||||
lyricWindow.CenterOnScreen()
|
||||
lyricWindow.SetOnClosed(func() {
|
||||
PlayController.LrcWindowOpen = false
|
||||
})
|
||||
return lyricWindow
|
||||
}
|
||||
19
gui/views/player/player.go
Normal file
19
gui/views/player/player.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
setupLyricViewer()
|
||||
registerHandlers()
|
||||
gctx.Context.OnMainWindowClosing(func() {
|
||||
if playerWindow != nil {
|
||||
gctx.Logger.Infof("closing player window")
|
||||
go playerWindow.Close()
|
||||
}
|
||||
})
|
||||
return container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist())
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package gui
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -28,12 +30,12 @@ func (b *playlistOperationButton) Tapped(e *fyne.PointEvent) {
|
||||
func newPlaylistOperationButton() *playlistOperationButton {
|
||||
b := &playlistOperationButton{Index: 0}
|
||||
deleteItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.delete"), func() {
|
||||
_ = global.EventBus.Publish(events.PlaylistDeleteCmd(model.PlaylistIDPlayer), events.PlaylistDeleteCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistDeleteCmd(model.PlaylistIDPlayer), events.PlaylistDeleteCmdEvent{
|
||||
Index: b.Index,
|
||||
})
|
||||
})
|
||||
topItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.top"), func() {
|
||||
_ = global.EventBus.Publish(events.PlaylistMoveCmd(model.PlaylistIDPlayer), events.PlaylistMoveCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistMoveCmd(model.PlaylistIDPlayer), events.PlaylistMoveCmdEvent{
|
||||
From: b.Index,
|
||||
To: 0,
|
||||
})
|
||||
@@ -61,9 +63,9 @@ func createPlaylist() fyne.CanvasObject {
|
||||
func() fyne.CanvasObject {
|
||||
return container.NewBorder(nil, nil, widget.NewLabel("index"), newPlaylistOperationButton(),
|
||||
container.NewGridWithColumns(3,
|
||||
newLabelWithWrapping("title", fyne.TextTruncate),
|
||||
newLabelWithWrapping("artist", fyne.TextTruncate),
|
||||
newLabelWithWrapping("user", fyne.TextTruncate)))
|
||||
component.NewLabelWithOpts("title", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("artist", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("user", component.LabelTruncation(fyne.TextTruncateClip))))
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
|
||||
@@ -75,7 +77,7 @@ func createPlaylist() fyne.CanvasObject {
|
||||
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
|
||||
object.(*fyne.Container).Objects[2].(*playlistOperationButton).Index = id
|
||||
})
|
||||
global.EventBus.Subscribe("", events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
UserPlaylist.mux.Lock()
|
||||
UserPlaylist.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias
|
||||
UserPlaylist.List.Refresh()
|
||||
@@ -1,14 +1,18 @@
|
||||
package gui
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
var playerWindow fyne.Window
|
||||
var playerWindowHandle uintptr
|
||||
|
||||
func setupPlayerWindow() {
|
||||
playerWindow = App.NewWindow("CorePlayerPreview")
|
||||
playerWindow = gctx.Context.App.NewWindow("CorePlayerPreview")
|
||||
playerWindow.Resize(fyne.NewSize(480, 240))
|
||||
playerWindow.SetCloseIntercept(func() {
|
||||
playerWindow.Hide()
|
||||
@@ -22,10 +26,10 @@ func showPlayerWindow() {
|
||||
}
|
||||
playerWindow.Show()
|
||||
if playerWindowHandle == 0 {
|
||||
playerWindowHandle = xfyne.GetWindowHandle(playerWindow)
|
||||
logger.Infof("video output window handle: %d", playerWindowHandle)
|
||||
playerWindowHandle = gutil.GetWindowHandle(playerWindow)
|
||||
gctx.Logger.Infof("video output window handle: %d", playerWindowHandle)
|
||||
if playerWindowHandle != 0 {
|
||||
_ = global.EventBus.Publish(events.PlayerVideoPlayerSetWindowHandleCmd,
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerVideoPlayerSetWindowHandleCmd,
|
||||
events.PlayerVideoPlayerSetWindowHandleCmdEvent{Handle: playerWindowHandle})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package gui
|
||||
package playlists
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fmt"
|
||||
@@ -33,6 +35,10 @@ type PlaylistsTab struct {
|
||||
|
||||
var PlaylistManager = &PlaylistsTab{}
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
return container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias())
|
||||
}
|
||||
|
||||
func createPlaylists() fyne.CanvasObject {
|
||||
PlaylistManager.Playlists = widget.NewList(
|
||||
func() int {
|
||||
@@ -46,7 +52,7 @@ func createPlaylists() fyne.CanvasObject {
|
||||
})
|
||||
PlaylistManager.AddBtn = widget.NewButton(i18n.T("gui.playlist.button.add"), func() {
|
||||
providerEntry := widget.NewSelect(PlaylistManager.providers, nil)
|
||||
idEntry := xfyne.EntryDisableUndoRedo(widget.NewEntry())
|
||||
idEntry := widget.NewEntry()
|
||||
dia := dialog.NewCustomConfirm(
|
||||
i18n.T("gui.playlist.add.title"),
|
||||
i18n.T("gui.playlist.add.confirm"),
|
||||
@@ -63,8 +69,8 @@ func createPlaylists() fyne.CanvasObject {
|
||||
),
|
||||
func(b bool) {
|
||||
if b && len(providerEntry.Selected) > 0 && len(idEntry.Text) > 0 {
|
||||
logger.Infof("add playlists %s %s", providerEntry.Selected, idEntry.Text)
|
||||
_ = global.EventBus.Publish(
|
||||
gctx.Logger.Infof("add playlists %s %s", providerEntry.Selected, idEntry.Text)
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.PlaylistManagerAddPlaylistCmd,
|
||||
events.PlaylistManagerAddPlaylistCmdEvent{
|
||||
Provider: providerEntry.Selected,
|
||||
@@ -72,7 +78,7 @@ func createPlaylists() fyne.CanvasObject {
|
||||
})
|
||||
}
|
||||
},
|
||||
MainWindow,
|
||||
gctx.Context.Window,
|
||||
)
|
||||
dia.Resize(fyne.NewSize(512, 256))
|
||||
dia.Show()
|
||||
@@ -81,8 +87,8 @@ func createPlaylists() fyne.CanvasObject {
|
||||
if PlaylistManager.Index >= len(PlaylistManager.currentPlaylists) {
|
||||
return
|
||||
}
|
||||
logger.Infof("remove playlists %s", PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID())
|
||||
_ = global.EventBus.Publish(
|
||||
gctx.Logger.Infof("remove playlists %s", PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID())
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.PlaylistManagerRemovePlaylistCmd,
|
||||
events.PlaylistManagerRemovePlaylistCmdEvent{
|
||||
PlaylistID: PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID(),
|
||||
@@ -93,29 +99,29 @@ func createPlaylists() fyne.CanvasObject {
|
||||
return
|
||||
}
|
||||
PlaylistManager.Index = id
|
||||
_ = global.EventBus.Publish(events.PlaylistManagerGetCurrentCmd, events.PlaylistManagerGetCurrentCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistManagerGetCurrentCmd, events.PlaylistManagerGetCurrentCmdEvent{
|
||||
PlaylistID: PlaylistManager.currentPlaylists[id].Meta.ID(),
|
||||
})
|
||||
}
|
||||
global.EventBus.Subscribe("", events.MediaProviderUpdate,
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.MediaProviderUpdate,
|
||||
"gui.playlists.provider.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
providers := event.Data.(events.MediaProviderUpdateEvent)
|
||||
s := make([]string, len(providers.Providers))
|
||||
copy(s, providers.Providers)
|
||||
PlaylistManager.providers = s
|
||||
}))
|
||||
global.EventBus.Subscribe("", events.PlaylistManagerInfoUpdate,
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlaylistManagerInfoUpdate,
|
||||
"gui.playlists.info.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlaylistManagerInfoUpdateEvent)
|
||||
prevLen := len(PlaylistManager.currentPlaylists)
|
||||
PlaylistManager.currentPlaylists = data.Playlists
|
||||
logger.Infof("receive playlist info update, try to refresh playlists. prevLen=%d, newLen=%d", prevLen, len(PlaylistManager.currentPlaylists))
|
||||
gctx.Logger.Infof("receive playlist info update, try to refresh playlists. prevLen=%d, newLen=%d", prevLen, len(PlaylistManager.currentPlaylists))
|
||||
PlaylistManager.Playlists.Refresh()
|
||||
if prevLen != len(PlaylistManager.currentPlaylists) {
|
||||
PlaylistManager.Playlists.Select(0)
|
||||
}
|
||||
}))
|
||||
global.EventBus.Subscribe("", events.PlaylistManagerSystemUpdate,
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlaylistManagerSystemUpdate,
|
||||
"gui.playlists.system.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlaylistManagerSystemUpdateEvent)
|
||||
PlaylistManager.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current") + data.Info.DisplayName())
|
||||
@@ -137,7 +143,7 @@ func createPlaylistMedias() fyne.CanvasObject {
|
||||
if PlaylistManager.Index >= len(PlaylistManager.currentPlaylists) {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlaylistManagerRefreshCurrentCmd, events.PlaylistManagerRefreshCurrentCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistManagerRefreshCurrentCmd, events.PlaylistManagerRefreshCurrentCmdEvent{
|
||||
PlaylistID: PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID(),
|
||||
})
|
||||
})
|
||||
@@ -147,8 +153,8 @@ func createPlaylistMedias() fyne.CanvasObject {
|
||||
if PlaylistManager.Index >= len(PlaylistManager.currentPlaylists) {
|
||||
return
|
||||
}
|
||||
logger.Infof("set playlist %s as system", PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID())
|
||||
_ = global.EventBus.Publish(events.PlaylistManagerSetSystemCmd, events.PlaylistManagerSetSystemCmdEvent{
|
||||
gctx.Logger.Infof("set playlist %s as system", PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID())
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistManagerSetSystemCmd, events.PlaylistManagerSetSystemCmdEvent{
|
||||
PlaylistID: PlaylistManager.currentPlaylists[PlaylistManager.Index].Meta.ID(),
|
||||
})
|
||||
})
|
||||
@@ -166,8 +172,8 @@ func createPlaylistMedias() fyne.CanvasObject {
|
||||
widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil),
|
||||
),
|
||||
container.NewGridWithColumns(2,
|
||||
newLabelWithWrapping("title", fyne.TextTruncate),
|
||||
newLabelWithWrapping("artist", fyne.TextTruncate)))
|
||||
component.NewLabelWithOpts("title", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("artist", component.LabelTruncation(fyne.TextTruncateClip))))
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
m := PlaylistManager.currentMedias[id]
|
||||
@@ -179,20 +185,20 @@ func createPlaylistMedias() fyne.CanvasObject {
|
||||
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
|
||||
m.User = model.SystemUser
|
||||
btns[0].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
|
||||
Media: m,
|
||||
})
|
||||
}
|
||||
btns[1].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
|
||||
Media: m,
|
||||
Position: -1,
|
||||
})
|
||||
}
|
||||
})
|
||||
global.EventBus.Subscribe("", events.PlaylistManagerCurrentUpdate,
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.PlaylistManagerCurrentUpdate,
|
||||
"gui.playlists.current.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
logger.Infof("receive current playlist update, try to refresh playlist medias")
|
||||
gctx.Logger.Infof("receive current playlist update, try to refresh playlist medias")
|
||||
data := event.Data.(events.PlaylistManagerCurrentUpdateEvent)
|
||||
PlaylistManager.currentMedias = data.Medias
|
||||
PlaylistManager.PlaylistMedia.Refresh()
|
||||
10
gui/views/search/search.go
Normal file
10
gui/views/search/search.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
return container.NewBorder(createSearchBar(), nil, nil, nil, createSearchList())
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package gui
|
||||
package search
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -30,18 +31,18 @@ func createSearchBar() fyne.CanvasObject {
|
||||
SearchBar.Button = widget.NewButton(i18n.T("gui.search.search"), func() {
|
||||
keyword := SearchBar.Input.Text
|
||||
pr := SearchBar.UseSource.Selected
|
||||
logger.Debugf("Search keyword: %s, provider: %s", keyword, pr)
|
||||
gctx.Logger.Debugf("Search keyword: %s, provider: %s", keyword, pr)
|
||||
SearchResult.mux.Lock()
|
||||
SearchResult.Items = make([]model.Media, 0)
|
||||
SearchResult.List.Refresh()
|
||||
SearchResult.mux.Unlock()
|
||||
_ = global.EventBus.Publish(events.SearchCmd, events.SearchCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.CmdMiaosicSearch, events.CmdMiaosicSearchData{
|
||||
Keyword: keyword,
|
||||
Provider: pr,
|
||||
})
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("", events.MediaProviderUpdate,
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.MediaProviderUpdate,
|
||||
"gui.search.provider.update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
providers := event.Data.(events.MediaProviderUpdateEvent)
|
||||
s := make([]string, len(providers.Providers))
|
||||
@@ -52,8 +53,7 @@ func createSearchBar() fyne.CanvasObject {
|
||||
}
|
||||
}))
|
||||
|
||||
SearchBar.UseSource = widget.NewSelect([]string{}, func(s string) {
|
||||
})
|
||||
SearchBar.UseSource = widget.NewSelect([]string{}, func(s string) {})
|
||||
|
||||
searchInput := container.NewBorder(
|
||||
nil, nil, widget.NewLabel(i18n.T("gui.search.search")), SearchBar.Button,
|
||||
@@ -1,9 +1,11 @@
|
||||
package gui
|
||||
package search
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -37,9 +39,9 @@ func createSearchList() fyne.CanvasObject {
|
||||
widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil),
|
||||
),
|
||||
container.NewGridWithColumns(3,
|
||||
newLabelWithWrapping("title", fyne.TextTruncate),
|
||||
newLabelWithWrapping("artist", fyne.TextTruncate),
|
||||
newLabelWithWrapping("source", fyne.TextTruncate)))
|
||||
component.NewLabelWithOpts("title", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("artist", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("user", component.LabelTruncation(fyne.TextTruncateClip))))
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
|
||||
@@ -51,19 +53,19 @@ func createSearchList() fyne.CanvasObject {
|
||||
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
|
||||
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
|
||||
btns[0].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
|
||||
Media: SearchResult.Items[id],
|
||||
})
|
||||
}
|
||||
btns[1].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.Publish(events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
|
||||
Media: SearchResult.Items[id],
|
||||
Position: -1,
|
||||
})
|
||||
}
|
||||
})
|
||||
global.EventBus.Subscribe("", events.SearchResultUpdate, "gui.search.update_result", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
items := event.Data.(events.SearchResultUpdateEvent).Medias
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.ReplyMiaosicSearch, "gui.search.update_result", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
items := event.Data.(events.ReplyMiaosicSearchData).Medias
|
||||
SearchResult.Items = items
|
||||
SearchResult.mux.Lock()
|
||||
SearchResult.List.Refresh()
|
||||
@@ -1,24 +1,23 @@
|
||||
package gui
|
||||
package systray
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"AynaLivePlayer/resource"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
)
|
||||
|
||||
func setupSysTray() {
|
||||
if desk, ok := App.(desktop.App); ok {
|
||||
func SetupSysTray() {
|
||||
if desk, ok := gctx.Context.App.(desktop.App); ok {
|
||||
m := fyne.NewMenu("MyApp",
|
||||
fyne.NewMenuItem(i18n.T("gui.tray.btn.show"), func() {
|
||||
MainWindow.Show()
|
||||
gctx.Context.Window.Show()
|
||||
}))
|
||||
desk.SetSystemTrayMenu(m)
|
||||
desk.SetSystemTrayIcon(resource.ImageIcon)
|
||||
gctx.Context.Window.SetCloseIntercept(func() {
|
||||
gctx.Context.Window.Hide()
|
||||
})
|
||||
}
|
||||
MainWindow.SetCloseIntercept(func() {
|
||||
_ = config.SaveToConfigFile(config.ConfigPath)
|
||||
MainWindow.Hide()
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package gui
|
||||
package updater
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -10,8 +11,8 @@ import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func checkUpdate() {
|
||||
global.EventBus.Subscribe("",
|
||||
func CreateUpdaterPopUp() {
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.CheckUpdateResultUpdate, "gui.updater.check_update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CheckUpdateResultUpdateEvent)
|
||||
msg := data.Info.Version.String() + "\n\n\n" + data.Info.Info
|
||||
@@ -20,13 +21,13 @@ func checkUpdate() {
|
||||
i18n.T("gui.update.new_version"),
|
||||
"OK",
|
||||
widget.NewRichTextFromMarkdown(msg),
|
||||
MainWindow)
|
||||
gctx.Context.Window)
|
||||
} else {
|
||||
dialog.ShowCustom(
|
||||
i18n.T("gui.update.already_latest_version"),
|
||||
"OK",
|
||||
widget.NewRichTextFromMarkdown(""),
|
||||
MainWindow)
|
||||
gctx.Context.Window)
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package xfyne
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func EntryDisableUndoRedo(entry *widget.Entry) *widget.Entry {
|
||||
// do nothing because the bug has been fixed in fyne@v2.5.1
|
||||
return entry
|
||||
//val := reflect.ValueOf(entry).Elem().FieldByName("shortcut").Addr().UnsafePointer()
|
||||
//(*fyne.ShortcutHandler)(val).RemoveShortcut(&fyne.ShortcutRedo{})
|
||||
//(*fyne.ShortcutHandler)(val).RemoveShortcut(&fyne.ShortcutUndo{})
|
||||
//return entry
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package xfyne
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEntryDisableUndoRedo(t *testing.T) {
|
||||
entry := widget.NewEntry()
|
||||
EntryDisableUndoRedo(entry)
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
)
|
||||
|
||||
func Initialize() {
|
||||
handleSearch()
|
||||
createLyricLoader()
|
||||
handlePlayNext()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
|
||||
"github.com/AynaLivePlayer/liveroom-sdk/provider/openblive"
|
||||
"github.com/AynaLivePlayer/liveroom-sdk/provider/webdm"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type liveroom struct {
|
||||
@@ -101,8 +102,8 @@ func addLiveRoom(roomModel model.LiveRoom) {
|
||||
|
||||
func registerHandlers() {
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomAddCmd, "internal.liveroom.add", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.LiveRoomAddCmdEvent)
|
||||
events.CmdLiveRoomAdd, "internal.liveroom.add", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CmdLiveRoomAddData)
|
||||
addLiveRoom(model.LiveRoom{
|
||||
LiveRoom: liveroomsdk.LiveRoom{
|
||||
Provider: data.Provider,
|
||||
@@ -117,8 +118,8 @@ func registerHandlers() {
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomRemoveCmd, "internal.liveroom.remove", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.LiveRoomRemoveCmdEvent)
|
||||
events.CmdLiveRoomRemove, "internal.liveroom.remove", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CmdLiveRoomRemoveData)
|
||||
room, ok := liveRooms[data.Identifier]
|
||||
if !ok {
|
||||
log.Errorf("remove room failed, room %s not found", data.Identifier)
|
||||
@@ -134,8 +135,8 @@ func registerHandlers() {
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomConfigChangeCmd, "internal.liveroom.config.change", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.LiveRoomConfigChangeCmdEvent)
|
||||
events.CmdLiveRoomConfigChange, "internal.liveroom.config.change", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CmdLiveRoomConfigChangeData)
|
||||
if room, ok := liveRooms[data.Identifier]; ok {
|
||||
room.model.Config = data.Config
|
||||
sendRoomStatusUpdateEvent(data.Identifier)
|
||||
@@ -143,8 +144,8 @@ func registerHandlers() {
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomOperationCmd, "internal.liveroom.operation", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.LiveRoomOperationCmdEvent)
|
||||
events.CmdLiveRoomOperation, "internal.liveroom.operation", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CmdLiveRoomOperationData)
|
||||
log.Infof("Live room operation SetConnect %v", data.SetConnect)
|
||||
room, ok := liveRooms[data.Identifier]
|
||||
if !ok {
|
||||
@@ -164,8 +165,8 @@ func registerHandlers() {
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomOperationFinish, events.LiveRoomOperationFinishEvent{})
|
||||
_ = global.EventBus.Reply(event,
|
||||
events.ReplyLiveRoomOperation, events.ReplyLiveRoomOperationData{})
|
||||
sendRoomStatusUpdateEvent(data.Identifier)
|
||||
})
|
||||
}
|
||||
@@ -178,8 +179,8 @@ func sendRoomStatusUpdateEvent(roomId string) {
|
||||
}
|
||||
log.Infof("send room status update event, room %s", roomId)
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomStatusUpdate,
|
||||
events.LiveRoomStatusUpdateEvent{
|
||||
events.UpdateLiveRoomStatus,
|
||||
events.UpdateLiveRoomStatusData{
|
||||
Room: room.model,
|
||||
})
|
||||
}
|
||||
@@ -189,9 +190,12 @@ func sendRoomsUpdateEvent() {
|
||||
for _, r := range liveRooms {
|
||||
rooms = append(rooms, r.model)
|
||||
}
|
||||
sort.Slice(rooms, func(i, j int) bool {
|
||||
return rooms[i].LiveRoom.Identifier() < rooms[j].LiveRoom.Identifier()
|
||||
})
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomRoomsUpdate,
|
||||
events.LiveRoomRoomsUpdateEvent{
|
||||
events.UpdateLiveRoomRooms,
|
||||
events.UpdateLiveRoomRoomsData{
|
||||
Rooms: rooms,
|
||||
})
|
||||
}
|
||||
@@ -218,8 +222,8 @@ func callEvents() {
|
||||
for _, r := range liveRooms {
|
||||
if r.model.Config.AutoConnect {
|
||||
_ = global.EventBus.Publish(
|
||||
events.LiveRoomOperationCmd,
|
||||
events.LiveRoomOperationCmdEvent{
|
||||
events.CmdLiveRoomOperation,
|
||||
events.CmdLiveRoomOperationData{
|
||||
Identifier: r.room.Config().Identifier(),
|
||||
SetConnect: true,
|
||||
})
|
||||
|
||||
@@ -199,16 +199,20 @@ func registerCmdHandler() {
|
||||
})
|
||||
mediaInfo := evnt.Data.(events.PlayerPlayCmdEvent).Media.Info
|
||||
media := evnt.Data.(events.PlayerPlayCmdEvent).Media
|
||||
if m, err := miaosic.GetMediaInfo(media.Info.Meta); err == nil {
|
||||
media.Info = m
|
||||
resp, err := global.EventBus.Call(events.CmdMiaosicGetMediaInfo, events.ReplyMiaosicGetMediaInfo,
|
||||
events.CmdMiaosicGetMediaInfoData{Meta: media.Info.Meta})
|
||||
if err == nil && resp.Data.(events.ReplyMiaosicGetMediaInfoData).Error == nil {
|
||||
media.Info = resp.Data.(events.ReplyMiaosicGetMediaInfoData).Info
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
|
||||
Media: media,
|
||||
Removed: false,
|
||||
})
|
||||
log.Infof("[MPV Player] Play media %s", mediaInfo.Title)
|
||||
mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny)
|
||||
if err != nil || len(mediaUrls) == 0 {
|
||||
resp, err = global.EventBus.Call(events.CmdMiaosicGetMediaUrl, events.ReplyMiaosicGetMediaUrl,
|
||||
events.CmdMiaosicGetMediaUrlData{Meta: media.Info.Meta, Quality: miaosic.QualityAny})
|
||||
mediaUrls := resp.Data.(events.ReplyMiaosicGetMediaUrlData)
|
||||
if err != nil || mediaUrls.Error != nil || len(mediaUrls.Urls) == 0 {
|
||||
log.Warn("[MPV PlayControl] get media url failed ", mediaInfo.Meta.ID(), err)
|
||||
if err := libmpv.Command([]string{"stop"}); err != nil {
|
||||
log.Error("[MPV PlayControl] failed to stop", err)
|
||||
@@ -220,7 +224,7 @@ func registerCmdHandler() {
|
||||
})
|
||||
return
|
||||
}
|
||||
mediaUrl := mediaUrls[0]
|
||||
mediaUrl := mediaUrls.Urls[0]
|
||||
if val, ok := mediaUrl.Header["User-Agent"]; ok {
|
||||
log.Debug("[MPV PlayControl] set user-agent for mpv player")
|
||||
err := libmpv.SetPropertyString("user-agent", val)
|
||||
|
||||
5
internal/player/readme.md
Normal file
5
internal/player/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# player
|
||||
|
||||
**player implementation**
|
||||
|
||||
for now, just use mpv, vlc not fully supported yet.
|
||||
@@ -5,13 +5,11 @@ import (
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
var PlayerPlaylist *playlist = nil
|
||||
var HistoryPlaylist *playlist = nil
|
||||
var SystemPlaylist *playlist = nil
|
||||
var PlaylistsPlaylist *playlist = nil
|
||||
|
||||
@@ -53,7 +51,6 @@ func Initialize() {
|
||||
log = global.Logger.WithPrefix("Playlists")
|
||||
PlayerPlaylist = newPlaylist(model.PlaylistIDPlayer)
|
||||
SystemPlaylist = newPlaylist(model.PlaylistIDSystem)
|
||||
HistoryPlaylist = newPlaylist(model.PlaylistIDHistory)
|
||||
config.LoadConfig(cfg)
|
||||
|
||||
_ = global.EventBus.Publish(events.PlaylistModeChangeCmd(model.PlaylistIDPlayer), events.PlaylistModeChangeCmdEvent{
|
||||
@@ -64,19 +61,6 @@ func Initialize() {
|
||||
Mode: cfg.SystemPlaylistMode,
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe("",
|
||||
events.PlayerPlayingUpdate,
|
||||
"internal.playlist.player_playing_update",
|
||||
func(event *eventbus.Event) {
|
||||
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlaylistInsertCmd(model.PlaylistIDHistory), events.PlaylistInsertCmdEvent{
|
||||
Media: event.Data.(events.PlayerPlayingUpdateEvent).Media,
|
||||
Position: -1,
|
||||
})
|
||||
})
|
||||
|
||||
createPlaylistManager()
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ func createPlaylistManager() {
|
||||
})
|
||||
return
|
||||
}
|
||||
// todo: use eventbus instead
|
||||
getPlaylist, err := miaosic.GetPlaylist(pl.Meta)
|
||||
if err != nil {
|
||||
_ = global.EventBus.Publish(
|
||||
@@ -156,6 +157,7 @@ func createPlaylistManager() {
|
||||
})
|
||||
return
|
||||
}
|
||||
// todo: use eventbus instead
|
||||
pl, err := miaosic.GetPlaylist(meta)
|
||||
if err != nil {
|
||||
_ = global.EventBus.Publish(
|
||||
|
||||
48
internal/source/base.go
Normal file
48
internal/source/base.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
type _sourceConfig struct {
|
||||
LocalSourcePath string
|
||||
QQChannel string
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) Name() string {
|
||||
return "Source"
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) OnLoad() {
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) OnSave() {
|
||||
}
|
||||
|
||||
var sourceCfg = &_sourceConfig{
|
||||
LocalSourcePath: "./music",
|
||||
QQChannel: "qq",
|
||||
}
|
||||
|
||||
var log logger.ILogger = nil
|
||||
|
||||
func Initialize() {
|
||||
config.LoadConfig(sourceCfg)
|
||||
|
||||
log = global.Logger.WithPrefix("MediaProvider")
|
||||
|
||||
loadMediaProvider()
|
||||
handleSearch()
|
||||
handleInfo()
|
||||
createLyricLoader()
|
||||
handleSourceLogin()
|
||||
|
||||
_ = global.EventBus.Publish(
|
||||
events.MediaProviderUpdate, events.MediaProviderUpdateEvent{
|
||||
Providers: miaosic.ListAvailableProviders(),
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
// dummySource is placeholder source for bypassing copyright requirement
|
||||
type dummySource struct{}
|
||||
|
||||
func (d *dummySource) GetName() string {
|
||||
|
||||
39
internal/source/info.go
Normal file
39
internal/source/info.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
func handleInfo() {
|
||||
err := global.EventBus.Subscribe("",
|
||||
events.CmdMiaosicGetMediaInfo, "internal.media_provider.getMediaInfo", func(event *eventbus.Event) {
|
||||
info, err := miaosic.GetMediaInfo(event.Data.(events.CmdMiaosicGetMediaInfoData).Meta)
|
||||
_ = global.EventBus.Reply(
|
||||
event, events.ReplyMiaosicGetMediaInfo,
|
||||
events.ReplyMiaosicGetMediaInfoData{
|
||||
Info: info,
|
||||
Error: err,
|
||||
},
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe search event failed", "error", err)
|
||||
}
|
||||
err = global.EventBus.Subscribe("",
|
||||
events.CmdMiaosicGetMediaUrl, "internal.media_provider.getMediaUrl", func(event *eventbus.Event) {
|
||||
urls, err := miaosic.GetMediaUrl(event.Data.(events.CmdMiaosicGetMediaUrlData).Meta, event.Data.(events.CmdMiaosicGetMediaUrlData).Quality)
|
||||
_ = global.EventBus.Reply(
|
||||
event, events.ReplyMiaosicGetMediaUrl,
|
||||
events.ReplyMiaosicGetMediaUrlData{
|
||||
Urls: urls,
|
||||
Error: err,
|
||||
},
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe search event failed", "error", err)
|
||||
}
|
||||
}
|
||||
50
internal/source/login.go
Normal file
50
internal/source/login.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
func handleSourceLogin() {
|
||||
err := global.EventBus.Subscribe("",
|
||||
events.CmdMiaosicQrLogin, "internal.media_provider.qrlogin_handler", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CmdMiaosicQrLoginData)
|
||||
log.Infof("trying login %s", data.Provider)
|
||||
pvdr, ok := miaosic.GetProvider(data.Provider)
|
||||
if !ok {
|
||||
_ = global.EventBus.Reply(
|
||||
event, events.ReplyMiaosicQrLogin,
|
||||
events.ReplyMiaosicQrLoginData{
|
||||
Session: miaosic.QrLoginSession{},
|
||||
Error: miaosic.ErrorNoSuchProvider,
|
||||
})
|
||||
return
|
||||
}
|
||||
result, ok := pvdr.(miaosic.Loginable)
|
||||
if !ok {
|
||||
_ = global.EventBus.Reply(
|
||||
event, events.ReplyMiaosicQrLogin,
|
||||
events.ReplyMiaosicQrLoginData{
|
||||
Session: miaosic.QrLoginSession{},
|
||||
Error: miaosic.ErrNotImplemented,
|
||||
})
|
||||
return
|
||||
}
|
||||
var session miaosic.QrLoginSession
|
||||
sess, err := result.QrLogin()
|
||||
if err == nil && sess != nil {
|
||||
session = *sess
|
||||
}
|
||||
_ = global.EventBus.Reply(
|
||||
event, events.ReplyMiaosicQrLogin,
|
||||
events.ReplyMiaosicQrLoginData{
|
||||
Session: session,
|
||||
Error: err,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe search event failed", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package controller
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
@@ -16,8 +16,8 @@ type lyricLoader struct {
|
||||
var lyricManager = &lyricLoader{}
|
||||
|
||||
func createLyricLoader() {
|
||||
log := global.Logger.WithPrefix("LyricLoader")
|
||||
global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "internal.lyric.update", func(event *eventbus.Event) {
|
||||
var err error
|
||||
err = global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "internal.lyric.update", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlayerPlayingUpdateEvent)
|
||||
if data.Removed {
|
||||
log.Debugf("current media removed, clear lyric")
|
||||
@@ -32,11 +32,14 @@ func createLyricLoader() {
|
||||
lyricManager.Lyric = miaosic.ParseLyrics("", "")
|
||||
log.Errorf("failed to get lyric for %s (%s): %s", data.Media.Info.Title, data.Media.Info.Meta.ID(), err)
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlayerLyricReload, events.PlayerLyricReloadEvent{
|
||||
_ = global.EventBus.Publish(events.UpdateCurrentLyric, events.UpdateCurrentLyricData{
|
||||
Lyrics: lyricManager.Lyric,
|
||||
})
|
||||
})
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "internal.lyric.update_current", func(event *eventbus.Event) {
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe player playing update event failed", "error", err)
|
||||
}
|
||||
err = global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "internal.lyric.update_current", func(event *eventbus.Event) {
|
||||
time := event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos
|
||||
idx := lyricManager.Lyric.FindIndex(time)
|
||||
if idx == lyricManager.prevIndex {
|
||||
@@ -53,9 +56,15 @@ func createLyricLoader() {
|
||||
})
|
||||
return
|
||||
})
|
||||
global.EventBus.Subscribe("", events.PlayerLyricRequestCmd, "internal.lyric.request", func(event *eventbus.Event) {
|
||||
_ = global.EventBus.Publish(events.PlayerLyricReload, events.PlayerLyricReloadEvent{
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe player time position update event failed", "error", err)
|
||||
}
|
||||
err = global.EventBus.Subscribe("", events.CmdGetCurrentLyric, "internal.lyric.request", func(event *eventbus.Event) {
|
||||
_ = global.EventBus.Reply(event, events.UpdateCurrentLyric, events.UpdateCurrentLyricData{
|
||||
Lyrics: lyricManager.Lyric,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe player lyric request command event failed", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -3,35 +3,9 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
)
|
||||
|
||||
type _sourceConfig struct {
|
||||
LocalSourcePath string
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) Name() string {
|
||||
return "Source"
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) OnLoad() {
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) OnSave() {
|
||||
}
|
||||
|
||||
var sourceCfg = &_sourceConfig{
|
||||
LocalSourcePath: "./music",
|
||||
}
|
||||
|
||||
func Initialize() {
|
||||
config.LoadConfig(sourceCfg)
|
||||
func loadMediaProvider() {
|
||||
miaosic.RegisterProvider(&dummySource{})
|
||||
_ = global.EventBus.Publish(
|
||||
events.MediaProviderUpdate, events.MediaProviderUpdateEvent{
|
||||
Providers: miaosic.ListAvailableProviders(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package controller
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
)
|
||||
|
||||
func handleSearch() {
|
||||
log := global.Logger.WithPrefix("Search")
|
||||
global.EventBus.Subscribe("",
|
||||
events.SearchCmd, "internal.controller.search.handleSearchCmd", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.SearchCmdEvent)
|
||||
err := global.EventBus.Subscribe("",
|
||||
events.CmdMiaosicSearch, "internal.media_provider.search_handler", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.CmdMiaosicSearchData)
|
||||
log.Infof("Search %s using %s", data.Keyword, data.Provider)
|
||||
searchResult, err := miaosic.SearchByProvider(data.Provider, data.Keyword, 1, 10)
|
||||
if err != nil {
|
||||
@@ -26,9 +25,13 @@ func handleSearch() {
|
||||
User: model.SystemUser,
|
||||
}
|
||||
}
|
||||
_ = global.EventBus.Publish(
|
||||
events.SearchResultUpdate, events.SearchResultUpdateEvent{
|
||||
_ = global.EventBus.Reply(
|
||||
event, events.ReplyMiaosicSearch,
|
||||
events.ReplyMiaosicSearchData{
|
||||
Medias: medias,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
log.ErrorW("Subscribe search event failed", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
_ "github.com/AynaLivePlayer/miaosic/providers/bilivideo"
|
||||
"github.com/AynaLivePlayer/miaosic/providers/kugou"
|
||||
@@ -15,38 +12,14 @@ import (
|
||||
"github.com/AynaLivePlayer/miaosic/providers/qq"
|
||||
)
|
||||
|
||||
type _sourceConfig struct {
|
||||
LocalSourcePath string
|
||||
QQChannel string
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) Name() string {
|
||||
return "Source"
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) OnLoad() {
|
||||
}
|
||||
|
||||
func (_ _sourceConfig) OnSave() {
|
||||
}
|
||||
|
||||
var sourceCfg = &_sourceConfig{
|
||||
LocalSourcePath: "./music",
|
||||
QQChannel: "qq",
|
||||
}
|
||||
|
||||
func Initialize() {
|
||||
config.LoadConfig(sourceCfg)
|
||||
func loadMediaProvider() {
|
||||
kugou.UseInstrumental()
|
||||
miaosic.RegisterProvider(local.NewLocal(sourceCfg.LocalSourcePath))
|
||||
if sourceCfg.QQChannel == "wechat" {
|
||||
log.Info("qqmusic: using wechat login channel")
|
||||
qq.UseWechatLogin()
|
||||
} else {
|
||||
log.Infof("qqmusic: using qq login channel")
|
||||
qq.UseQQLogin()
|
||||
}
|
||||
|
||||
_ = global.EventBus.Publish(
|
||||
events.MediaProviderUpdate, events.MediaProviderUpdateEvent{
|
||||
Providers: miaosic.ListAvailableProviders(),
|
||||
})
|
||||
}
|
||||
|
||||
266
internal/sysmediacontrol/smc_darwin.go
Normal file
266
internal/sysmediacontrol/smc_darwin.go
Normal file
@@ -0,0 +1,266 @@
|
||||
//go:build darwin
|
||||
|
||||
package sysmediacontrol
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework MediaPlayer -framework AppKit
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
// Forward declaration for Go export function
|
||||
extern void handleCommand(int);
|
||||
|
||||
// Command handler
|
||||
static MPRemoteCommandHandlerStatus commandHandler(MPRemoteCommandEvent *event, int command) {
|
||||
handleCommand(command);
|
||||
return MPRemoteCommandHandlerStatusSuccess;
|
||||
}
|
||||
|
||||
// Initialize media player controls
|
||||
static void initMediaPlayer() {
|
||||
@autoreleasepool {
|
||||
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
|
||||
|
||||
[[commandCenter playCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||
return commandHandler(event, 0); // 0 = play
|
||||
}];
|
||||
|
||||
[[commandCenter pauseCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||
return commandHandler(event, 1); // 1 = pause
|
||||
}];
|
||||
|
||||
[[commandCenter nextTrackCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||
return commandHandler(event, 2); // 2 = next
|
||||
}];
|
||||
|
||||
[[commandCenter previousTrackCommand] addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
|
||||
return commandHandler(event, 3); // 3 = previous
|
||||
}];
|
||||
|
||||
// Enable commands
|
||||
[commandCenter playCommand].enabled = YES;
|
||||
[commandCenter pauseCommand].enabled = YES;
|
||||
[commandCenter nextTrackCommand].enabled = YES;
|
||||
[commandCenter previousTrackCommand].enabled = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Update now playing info
|
||||
static void updateNowPlaying(const char *title, const char *artist, const char *album,
|
||||
double duration, double position, int isPlaying) {
|
||||
@autoreleasepool {
|
||||
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
|
||||
NSMutableDictionary *nowPlayingInfo = [center.nowPlayingInfo mutableCopy];
|
||||
if (nowPlayingInfo == nil) {
|
||||
nowPlayingInfo = [NSMutableDictionary dictionary];
|
||||
}
|
||||
|
||||
if (title != NULL) {
|
||||
[nowPlayingInfo setObject:[NSString stringWithUTF8String:title]
|
||||
forKey:MPMediaItemPropertyTitle];
|
||||
}
|
||||
|
||||
if (artist != NULL) {
|
||||
[nowPlayingInfo setObject:[NSString stringWithUTF8String:artist]
|
||||
forKey:MPMediaItemPropertyArtist];
|
||||
}
|
||||
|
||||
if (album != NULL) {
|
||||
[nowPlayingInfo setObject:[NSString stringWithUTF8String:album]
|
||||
forKey:MPMediaItemPropertyAlbumTitle];
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
[nowPlayingInfo setObject:[NSNumber numberWithDouble:duration]
|
||||
forKey:MPMediaItemPropertyPlaybackDuration];
|
||||
}
|
||||
|
||||
if (position >= 0) {
|
||||
[nowPlayingInfo setObject:[NSNumber numberWithDouble:position]
|
||||
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
|
||||
}
|
||||
|
||||
[nowPlayingInfo setObject:[NSNumber numberWithDouble:(isPlaying ? 1.0 : 0.0)]
|
||||
forKey:MPNowPlayingInfoPropertyPlaybackRate];
|
||||
|
||||
center.nowPlayingInfo = nowPlayingInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// Update artwork from URL
|
||||
static void updateArtworkFromURL(const char *urlString) {
|
||||
@autoreleasepool {
|
||||
if (urlString == NULL) return;
|
||||
|
||||
NSString *urlStr = [NSString stringWithUTF8String:urlString];
|
||||
NSURL *url = [NSURL URLWithString:urlStr];
|
||||
if (url == NULL) return;
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSData *imageData = [NSData dataWithContentsOfURL:url];
|
||||
if (imageData) {
|
||||
NSImage *image = [[NSImage alloc] initWithData:imageData];
|
||||
if (image) {
|
||||
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc]
|
||||
initWithBoundsSize:image.size
|
||||
requestHandler:^NSImage * _Nonnull(CGSize size) {
|
||||
return image;
|
||||
}];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
|
||||
NSMutableDictionary *nowPlayingInfo = [center.nowPlayingInfo mutableCopy];
|
||||
if (nowPlayingInfo == nil) {
|
||||
nowPlayingInfo = [NSMutableDictionary dictionary];
|
||||
}
|
||||
[nowPlayingInfo setObject:artwork forKey:MPMediaItemPropertyArtwork];
|
||||
center.nowPlayingInfo = nowPlayingInfo;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear now playing info
|
||||
static void clearNowPlaying() {
|
||||
@autoreleasepool {
|
||||
MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
|
||||
center.nowPlayingInfo = nil;
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
log logger.ILogger
|
||||
currentTitle string
|
||||
currentArtist string
|
||||
currentAlbum string
|
||||
currentDuration float64
|
||||
currentPosition float64
|
||||
currentIsPlaying bool
|
||||
)
|
||||
|
||||
//export handleCommand
|
||||
func handleCommand(command C.int) {
|
||||
switch command {
|
||||
case 0: // Play
|
||||
_ = global.EventBus.Publish(
|
||||
events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: false})
|
||||
case 1: // Pause
|
||||
_ = global.EventBus.Publish(
|
||||
events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: true})
|
||||
case 2: // Next
|
||||
_ = global.EventBus.Publish(
|
||||
events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
|
||||
case 3: // Previous
|
||||
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: 0,
|
||||
Absolute: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateNowPlayingInfo() {
|
||||
titleC := C.CString(currentTitle)
|
||||
artistC := C.CString(currentArtist)
|
||||
albumC := C.CString(currentAlbum)
|
||||
defer C.free(unsafe.Pointer(titleC))
|
||||
defer C.free(unsafe.Pointer(artistC))
|
||||
defer C.free(unsafe.Pointer(albumC))
|
||||
|
||||
isPlaying := 0
|
||||
if currentIsPlaying {
|
||||
isPlaying = 1
|
||||
}
|
||||
|
||||
C.updateNowPlaying(
|
||||
titleC,
|
||||
artistC,
|
||||
albumC,
|
||||
C.double(currentDuration),
|
||||
C.double(currentPosition),
|
||||
C.int(isPlaying),
|
||||
)
|
||||
}
|
||||
|
||||
func InitSystemMediaControl() {
|
||||
log = global.Logger.WithPrefix("SMTC-Darwin")
|
||||
|
||||
// Initialize media player controls
|
||||
C.initMediaPlayer()
|
||||
|
||||
// Subscribe to player playing update events
|
||||
global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "sysmediacontrol.update_playing", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlayerPlayingUpdateEvent)
|
||||
|
||||
if data.Removed {
|
||||
C.clearNowPlaying()
|
||||
currentTitle = ""
|
||||
currentArtist = ""
|
||||
currentAlbum = ""
|
||||
currentDuration = 0
|
||||
currentPosition = 0
|
||||
return
|
||||
}
|
||||
|
||||
currentTitle = data.Media.Info.Title
|
||||
currentArtist = data.Media.Info.Artist
|
||||
currentAlbum = data.Media.Info.Album
|
||||
|
||||
updateNowPlayingInfo()
|
||||
|
||||
// Update artwork if available
|
||||
if data.Media.Info.Cover.Url != "" {
|
||||
urlC := C.CString(data.Media.Info.Cover.Url)
|
||||
C.updateArtworkFromURL(urlC)
|
||||
C.free(unsafe.Pointer(urlC))
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to pause state updates
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyPauseUpdate, "sysmediacontrol.update_paused", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlayerPropertyPauseUpdateEvent)
|
||||
currentIsPlaying = !data.Paused
|
||||
updateNowPlayingInfo()
|
||||
})
|
||||
|
||||
// Subscribe to duration updates
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyDurationUpdate, "sysmediacontrol.properties.duration", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlayerPropertyDurationUpdateEvent)
|
||||
currentDuration = data.Duration
|
||||
updateNowPlayingInfo()
|
||||
})
|
||||
|
||||
// Subscribe to time position updates
|
||||
global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "sysmediacontrol.properties.time_pos", func(event *eventbus.Event) {
|
||||
data := event.Data.(events.PlayerPropertyTimePosUpdateEvent)
|
||||
currentPosition = data.TimePos
|
||||
updateNowPlayingInfo()
|
||||
})
|
||||
|
||||
log.Info("macOS System Media Control initialized")
|
||||
}
|
||||
|
||||
func Destroy() {
|
||||
C.clearNowPlaying()
|
||||
|
||||
// Unsubscribe from all events
|
||||
global.EventBus.Unsubscribe(events.PlayerPlayingUpdate, "sysmediacontrol.update_playing")
|
||||
global.EventBus.Unsubscribe(events.PlayerPropertyPauseUpdate, "sysmediacontrol.update_paused")
|
||||
global.EventBus.Unsubscribe(events.PlayerPropertyDurationUpdate, "sysmediacontrol.properties.duration")
|
||||
global.EventBus.Unsubscribe(events.PlayerPropertyTimePosUpdate, "sysmediacontrol.properties.time_pos")
|
||||
|
||||
log.Info("macOS System Media Control destroyed")
|
||||
}
|
||||
@@ -26,7 +26,7 @@ type Subscriber interface {
|
||||
// SubscribeAny is Subscribe with empty channel. this function will subscribe to event from any channel
|
||||
SubscribeAny(eventId string, handlerName string, fn HandlerFunc) error
|
||||
// SubscribeOnce will run handler once, and delete handler internally
|
||||
SubscribeOnce(eventId string, handlerName string, fn HandlerFunc) error
|
||||
SubscribeOnce(channel string, eventId string, handlerName string, fn HandlerFunc) error
|
||||
// Unsubscribe just remove handler for the bus
|
||||
Unsubscribe(eventId string, handlerName string) error
|
||||
}
|
||||
@@ -34,13 +34,16 @@ type Subscriber interface {
|
||||
type Publisher interface {
|
||||
// Publish basically a wrapper to PublishEvent
|
||||
Publish(eventId string, data interface{}) error
|
||||
// PublishToChannel publish event to a specific channel, basically another wrapper to PublishEvent
|
||||
PublishToChannel(channel string, eventId string, data interface{}) error
|
||||
// PublishEvent publish an event
|
||||
PublishEvent(event *Event) error
|
||||
}
|
||||
|
||||
// Caller is special usage of a Publisher
|
||||
type Caller interface {
|
||||
Call(pubEvtId string, data interface{}, subEvtId string) (*Event, error)
|
||||
Call(pubEvtId string, subEvtId string, data interface{}) (*Event, error)
|
||||
Reply(req *Event, eventId string, data interface{}) error
|
||||
}
|
||||
|
||||
type Controller interface {
|
||||
|
||||
@@ -3,7 +3,6 @@ package eventbus
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -24,8 +23,8 @@ type task struct {
|
||||
// bus implements Bus.
|
||||
type bus struct {
|
||||
// configuration
|
||||
workerCount int
|
||||
queueSize int
|
||||
maxWorkerSize int
|
||||
queueSize int
|
||||
|
||||
// workers
|
||||
queues []chan task
|
||||
@@ -39,9 +38,10 @@ type bus struct {
|
||||
drainedCh chan struct{}
|
||||
|
||||
// routing & bookkeeping
|
||||
mu sync.RWMutex
|
||||
handlers map[string]map[string]handlerRec // eventId -> handlerName -> handlerRec
|
||||
pending []*Event // events published before Start()
|
||||
mu sync.RWMutex
|
||||
handlers map[string]map[string]handlerRec // eventId -> handlerName -> handlerRec
|
||||
workerIdxes map[string]int // eventId -> workerIdx
|
||||
pending []*Event // events published before Start()
|
||||
|
||||
// rendezvous for Call/EchoId
|
||||
waitMu sync.Mutex
|
||||
@@ -57,32 +57,39 @@ type bus struct {
|
||||
// workerCount >= 1, queueSize >= 1.
|
||||
func New(opts ...Option) Bus {
|
||||
option := options{
|
||||
log: Log,
|
||||
workerSize: 10,
|
||||
queueSize: 100,
|
||||
log: Log,
|
||||
maxWorkerSize: 10,
|
||||
queueSize: 100,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&option)
|
||||
}
|
||||
b := &bus{
|
||||
workerCount: option.workerSize,
|
||||
queueSize: option.queueSize,
|
||||
queues: make([]chan task, option.workerSize),
|
||||
stopCh: make(chan struct{}),
|
||||
drainedCh: make(chan struct{}),
|
||||
handlers: make(map[string]map[string]handlerRec),
|
||||
pending: make([]*Event, 0, 16),
|
||||
echoWaiter: make(map[string]chan *Event),
|
||||
log: option.log,
|
||||
maxWorkerSize: option.maxWorkerSize,
|
||||
queueSize: option.queueSize,
|
||||
queues: make([]chan task, 0, option.maxWorkerSize),
|
||||
stopCh: make(chan struct{}),
|
||||
drainedCh: make(chan struct{}),
|
||||
handlers: make(map[string]map[string]handlerRec),
|
||||
workerIdxes: make(map[string]int),
|
||||
pending: make([]*Event, 0, 16),
|
||||
echoWaiter: make(map[string]chan *Event),
|
||||
log: option.log,
|
||||
}
|
||||
for i := 0; i < option.workerSize; i++ {
|
||||
q := make(chan task, option.queueSize)
|
||||
b.queues[i] = q
|
||||
go b.workerLoop(q)
|
||||
for i := 0; i < option.maxWorkerSize; i++ {
|
||||
b.addWorker()
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *bus) addWorker() {
|
||||
b.mu.Lock()
|
||||
q := make(chan task, b.queueSize)
|
||||
b.queues = append(b.queues, q)
|
||||
go b.workerLoop(q)
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *bus) workerLoop(q chan task) {
|
||||
for {
|
||||
select {
|
||||
@@ -145,18 +152,13 @@ func (b *bus) Wait() error {
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-b.drainedCh:
|
||||
// Stopped
|
||||
<-done
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bus) Stop() error {
|
||||
b.stopOnce.Do(func() {
|
||||
b.stopping.Store(true)
|
||||
close(b.stopCh) // signal workers to stop immediately
|
||||
close(b.drainedCh) // allow Wait() to proceed
|
||||
close(b.stopCh) // signal workers to stop immediately
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -171,27 +173,17 @@ func (b *bus) Subscribe(channel string, eventId, handlerName string, fn HandlerF
|
||||
if m == nil {
|
||||
m = make(map[string]handlerRec)
|
||||
b.handlers[eventId] = m
|
||||
b.workerIdxes[eventId] = len(b.workerIdxes) % b.maxWorkerSize // assign a worker index for this eventId
|
||||
}
|
||||
m[handlerName] = handlerRec{name: handlerName, fn: fn, channel: channel}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bus) SubscribeAny(eventId, handlerName string, fn HandlerFunc) error {
|
||||
if eventId == "" || handlerName == "" || fn == nil {
|
||||
return errors.New("invalid Subscribe args")
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
m := b.handlers[eventId]
|
||||
if m == nil {
|
||||
m = make(map[string]handlerRec)
|
||||
b.handlers[eventId] = m
|
||||
}
|
||||
m[handlerName] = handlerRec{name: handlerName, fn: fn, channel: ""}
|
||||
return nil
|
||||
return b.Subscribe("", eventId, handlerName, fn)
|
||||
}
|
||||
|
||||
func (b *bus) SubscribeOnce(eventId, handlerName string, fn HandlerFunc) error {
|
||||
func (b *bus) SubscribeOnce(channel, eventId, handlerName string, fn HandlerFunc) error {
|
||||
if eventId == "" || handlerName == "" || fn == nil {
|
||||
return errors.New("invalid SubscribeOnce args")
|
||||
}
|
||||
@@ -201,8 +193,9 @@ func (b *bus) SubscribeOnce(eventId, handlerName string, fn HandlerFunc) error {
|
||||
if m == nil {
|
||||
m = make(map[string]handlerRec)
|
||||
b.handlers[eventId] = m
|
||||
b.workerIdxes[eventId] = len(b.workerIdxes) % b.maxWorkerSize // assign a worker index for this eventId
|
||||
}
|
||||
m[handlerName] = handlerRec{name: handlerName, fn: fn, once: true}
|
||||
m[handlerName] = handlerRec{channel: channel, name: handlerName, fn: fn, once: true}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -225,6 +218,10 @@ func (b *bus) Publish(eventId string, data interface{}) error {
|
||||
return b.PublishEvent(&Event{Id: eventId, Data: data})
|
||||
}
|
||||
|
||||
func (b *bus) PublishToChannel(channel string, eventId string, data interface{}) error {
|
||||
return b.PublishEvent(&Event{Id: eventId, Channel: channel, Data: data})
|
||||
}
|
||||
|
||||
func (b *bus) PublishEvent(ev *Event) error {
|
||||
if ev == nil || ev.Id == "" {
|
||||
return errors.New("invalid PublishEvent args")
|
||||
@@ -242,6 +239,10 @@ func (b *bus) PublishEvent(ev *Event) error {
|
||||
case ch <- ev:
|
||||
default:
|
||||
}
|
||||
// in this case, we found this event belong to local call
|
||||
// so we don't need to dispatch this event to other subscriber
|
||||
b.waitMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
b.waitMu.Unlock()
|
||||
}
|
||||
@@ -281,7 +282,7 @@ func (b *bus) PublishEvent(ev *Event) error {
|
||||
|
||||
// Enqueue each handler on its shard (worker) based on (eventId, handlerName).
|
||||
for _, h := range hs {
|
||||
idx := shardIndex(b.workerCount, ev.Id, h.name)
|
||||
idx := b.shardIndex(ev.Id, h.name)
|
||||
b.wg.Add(1)
|
||||
select {
|
||||
case b.queues[idx] <- task{ev: cloneEvent(ev), h: h}:
|
||||
@@ -296,7 +297,7 @@ func (b *bus) PublishEvent(ev *Event) error {
|
||||
// Call publishes a request and waits for a response event with the same EchoId.
|
||||
// NOTE: Handlers should reply by publishing an Event with the SAME EchoId.
|
||||
// Use Reply helper below.
|
||||
func (b *bus) Call(eventId string, data interface{}, subEvtId string) (*Event, error) {
|
||||
func (b *bus) Call(eventId string, subEvtId string, data interface{}) (*Event, error) {
|
||||
if eventId == "" {
|
||||
return nil, errors.New("empty eventId")
|
||||
}
|
||||
@@ -322,22 +323,37 @@ func (b *bus) Call(eventId string, data interface{}, subEvtId string) (*Event, e
|
||||
return resp, nil
|
||||
case <-timeout:
|
||||
return nil, errors.New("call timeout")
|
||||
case <-b.drainedCh:
|
||||
case <-b.stopCh:
|
||||
return nil, errors.New("bus stopped")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bus) Reply(req *Event, eventId string, data interface{}) error {
|
||||
return b.PublishEvent(&Event{
|
||||
Id: eventId,
|
||||
Channel: req.Channel,
|
||||
EchoId: req.EchoId,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *bus) nextEchoId() string {
|
||||
x := b.idCtr.Add(1)
|
||||
return fmt.Sprintf("echo-%d-%d", time.Now().UnixNano(), x)
|
||||
}
|
||||
|
||||
func shardIndex(n int, eventId, handlerName string) int {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(eventId))
|
||||
_, _ = h.Write([]byte{0})
|
||||
_, _ = h.Write([]byte(handlerName))
|
||||
return int(h.Sum32() % uint32(n))
|
||||
func (b *bus) shardIndex(eventId, handlerName string) int {
|
||||
val, _ := b.workerIdxes[eventId]
|
||||
return val
|
||||
// what if two different eventId and handlerName produce same shard index?
|
||||
// and one handler happens to call another event synchronously?
|
||||
// well, in that case, the second event will be blocked until the first one finishes
|
||||
// which cause deadlock if the first one is waiting for the second one to finish
|
||||
//h := fnv.New32a()
|
||||
//_, _ = h.Write([]byte(eventId))
|
||||
//_, _ = h.Write([]byte{0})
|
||||
//_, _ = h.Write([]byte(handlerName))
|
||||
//return int(h.Sum32() % uint32(n))
|
||||
}
|
||||
|
||||
func cloneEvent(e *Event) *Event {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// TestBasicLifecycle verifies the fundamental Start, Stop, and Wait operations.
|
||||
func TestBasicLifecycle(t *testing.T) {
|
||||
b := New(WithWorkerSize(2), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(2), WithQueueSize(10))
|
||||
|
||||
// Start should only work once.
|
||||
err := b.Start()
|
||||
@@ -33,7 +33,7 @@ func TestBasicLifecycle(t *testing.T) {
|
||||
// TestSubscribeAndPublish verifies the core functionality of publishing an event
|
||||
// and having a subscriber receive it.
|
||||
func TestSubscribeAndPublish(t *testing.T) {
|
||||
b := New(WithWorkerSize(1), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(1), WithQueueSize(10))
|
||||
err := b.Start()
|
||||
require.NoError(t, err)
|
||||
defer b.Stop()
|
||||
@@ -59,7 +59,7 @@ func TestSubscribeAndPublish(t *testing.T) {
|
||||
|
||||
// TestUnsubscribe ensures that a handler stops receiving events after unsubscribing.
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
b := New(WithWorkerSize(2), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(2), WithQueueSize(10))
|
||||
b.Start()
|
||||
defer b.Stop()
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestUnsubscribe(t *testing.T) {
|
||||
// TestSubscribeOnce verifies that a handler subscribed with SubscribeOnce
|
||||
// is only called once and then automatically removed.
|
||||
func TestSubscribeOnce(t *testing.T) {
|
||||
b := New(WithWorkerSize(1), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(1), WithQueueSize(10))
|
||||
b.Start()
|
||||
defer b.Stop()
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestSubscribeOnce(t *testing.T) {
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
err := b.SubscribeOnce("event-once", "handler-once", handler)
|
||||
err := b.SubscribeOnce("", "event-once", "handler-once", handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Publish twice
|
||||
@@ -118,7 +118,7 @@ func TestSubscribeOnce(t *testing.T) {
|
||||
|
||||
// TestChannelSubscription validates that handlers correctly receive events based on channel matching.
|
||||
func TestChannelSubscription(t *testing.T) {
|
||||
b := New(WithWorkerSize(2), WithQueueSize(20))
|
||||
b := New(WithMaxWorkerSize(2), WithQueueSize(20))
|
||||
b.Start()
|
||||
defer b.Stop()
|
||||
|
||||
@@ -184,7 +184,7 @@ func TestChannelSubscription(t *testing.T) {
|
||||
// TestPublishBeforeStart ensures that events published before the bus starts are queued
|
||||
// and processed after Start() is called.
|
||||
func TestPublishBeforeStart(t *testing.T) {
|
||||
b := New(WithWorkerSize(1), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(1), WithQueueSize(10))
|
||||
|
||||
var receivedCount int32
|
||||
var wg sync.WaitGroup
|
||||
@@ -215,7 +215,7 @@ func TestPublishBeforeStart(t *testing.T) {
|
||||
|
||||
// TestCall validates the request-response pattern using the Call method.
|
||||
func TestCall(t *testing.T) {
|
||||
b := New(WithWorkerSize(2), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(2), WithQueueSize(10))
|
||||
b.Start()
|
||||
defer b.Stop()
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestCall(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make the call
|
||||
resp, err := b.Call("request-event", "my-data", "response-event")
|
||||
resp, err := b.Call("request-event", "response-event", "my-data")
|
||||
|
||||
// Verify response
|
||||
require.NoError(t, err)
|
||||
@@ -245,7 +245,7 @@ func TestCall(t *testing.T) {
|
||||
|
||||
// TestCall_StopDuringWait checks that Call returns an error if the bus is stopped while waiting.
|
||||
func TestCall_StopDuringWait(t *testing.T) {
|
||||
b := New(WithWorkerSize(1), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(1), WithQueueSize(10))
|
||||
b.Start()
|
||||
|
||||
var callErr error
|
||||
@@ -255,7 +255,7 @@ func TestCall_StopDuringWait(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// This call will never get a response
|
||||
_, callErr = b.Call("no-reply-event", nil, "no-reply-response")
|
||||
_, callErr = b.Call("no-reply-event", "no-reply-response", nil)
|
||||
}()
|
||||
|
||||
// Give the goroutine time to start waiting
|
||||
@@ -269,7 +269,7 @@ func TestCall_StopDuringWait(t *testing.T) {
|
||||
|
||||
// TestPanicRecovery ensures that a panicking handler does not crash the worker.
|
||||
func TestPanicRecovery(t *testing.T) {
|
||||
b := New(WithWorkerSize(1), WithQueueSize(10))
|
||||
b := New(WithMaxWorkerSize(1), WithQueueSize(10))
|
||||
b.Start()
|
||||
defer b.Stop()
|
||||
|
||||
@@ -304,7 +304,7 @@ func TestPanicRecovery(t *testing.T) {
|
||||
func TestConcurrency(t *testing.T) {
|
||||
workerCount := 4
|
||||
queueSize := 50
|
||||
b := New(WithWorkerSize(workerCount), WithQueueSize(queueSize))
|
||||
b := New(WithMaxWorkerSize(workerCount), WithQueueSize(queueSize))
|
||||
b.Start()
|
||||
defer b.Stop()
|
||||
|
||||
@@ -368,7 +368,7 @@ func TestConcurrency(t *testing.T) {
|
||||
|
||||
// TestInvalidArguments checks that API methods return errors on invalid input.
|
||||
func TestInvalidArguments(t *testing.T) {
|
||||
b := New(WithWorkerSize(1), WithQueueSize(1))
|
||||
b := New(WithMaxWorkerSize(1), WithQueueSize(1))
|
||||
|
||||
// Subscribe
|
||||
err := b.Subscribe("", "", "name", func(e *Event) {})
|
||||
@@ -383,7 +383,7 @@ func TestInvalidArguments(t *testing.T) {
|
||||
require.Error(t, err, "SubscribeAny should error on empty eventId")
|
||||
|
||||
// SubscribeOnce
|
||||
err = b.SubscribeOnce("", "name", func(e *Event) {})
|
||||
err = b.SubscribeOnce("", "", "name", func(e *Event) {})
|
||||
require.Error(t, err, "SubscribeOnce should error on empty eventId")
|
||||
|
||||
// Unsubscribe
|
||||
@@ -393,6 +393,6 @@ func TestInvalidArguments(t *testing.T) {
|
||||
require.Error(t, err, "Unsubscribe should error on empty handlerName")
|
||||
|
||||
// Call
|
||||
_, err = b.Call("", nil, "subID")
|
||||
_, err = b.Call("", "subID", nil)
|
||||
require.Error(t, err, "Call should error on empty eventId")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package eventbus
|
||||
|
||||
type options struct {
|
||||
log Logger
|
||||
workerSize int
|
||||
queueSize int
|
||||
log Logger
|
||||
maxWorkerSize int
|
||||
queueSize int
|
||||
}
|
||||
|
||||
type Option func(*options)
|
||||
@@ -12,10 +12,10 @@ func WithLogger(logger Logger) Option {
|
||||
return func(o *options) { o.log = logger }
|
||||
}
|
||||
|
||||
func WithWorkerSize(workerSize int) Option {
|
||||
func WithMaxWorkerSize(maxWorkerSize int) Option {
|
||||
return func(o *options) {
|
||||
if workerSize >= 1 {
|
||||
o.workerSize = workerSize
|
||||
if maxWorkerSize >= 1 {
|
||||
o.maxWorkerSize = maxWorkerSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule pkg/miaosic updated: 8f67b50eae...2e615a15d7
@@ -11,7 +11,7 @@ func main() {
|
||||
a := app.New()
|
||||
w := a.NewWindow("SysTray")
|
||||
|
||||
icon, _ := fyne.LoadResourceFromPath("./assets/icon.jpg")
|
||||
icon, _ := fyne.LoadResourceFromPath("./assets/icon2.jpg")
|
||||
//icon, _ := fyne.LoadResourceFromPath("./assets/icon.png")
|
||||
|
||||
if desk, ok := a.(desktop.App); ok {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package diange
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
@@ -31,7 +30,7 @@ func (b *blacklist) CreatePanel() fyne.CanvasObject {
|
||||
return b.panel
|
||||
}
|
||||
// UI组件
|
||||
input := xfyne.EntryDisableUndoRedo(widget.NewEntry())
|
||||
input := widget.NewEntry()
|
||||
input.SetPlaceHolder(i18n.T("plugin.diange.blacklist.input.placeholder"))
|
||||
|
||||
exactText := i18n.T("plugin.diange.blacklist.option.exact")
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -129,8 +129,8 @@ func (c *Diange) OnSave() {
|
||||
|
||||
func (d *Diange) Enable() error {
|
||||
config.LoadConfig(d)
|
||||
gui.AddConfigLayout(d)
|
||||
gui.AddConfigLayout(&blacklist{})
|
||||
config2.AddConfigLayout(d)
|
||||
config2.AddConfigLayout(&blacklist{})
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomMessageReceive,
|
||||
"plugin.diange.message",
|
||||
@@ -382,27 +382,27 @@ func (d *Diange) CreatePanel() fyne.CanvasObject {
|
||||
container.NewGridWithColumns(2,
|
||||
container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.diange.medal.name")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.BindString(&d.MedalName)))),
|
||||
widget.NewEntryWithData(binding.BindString(&d.MedalName))),
|
||||
container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.diange.medal.level")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.MedalPermission))))),
|
||||
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.MedalPermission)))),
|
||||
),
|
||||
)
|
||||
dgQueue := container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.diange.queue_max")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.QueueMax)))),
|
||||
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.QueueMax))),
|
||||
)
|
||||
dgUserMax := container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.diange.user_max")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.UserMax)))),
|
||||
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.UserMax))),
|
||||
)
|
||||
dgCoolDown := container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.diange.cooldown")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.UserCoolDown)))),
|
||||
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.UserCoolDown))),
|
||||
)
|
||||
dgShortCut := container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.diange.custom_cmd")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.BindString(&d.CustomCMD))),
|
||||
widget.NewEntryWithData(binding.BindString(&d.CustomCMD)),
|
||||
)
|
||||
skipPlaylistCheck := widget.NewCheckWithData(i18n.T("plugin.diange.skip_playlist.prompt"), binding.BindBool(&d.SkipSystemPlaylist))
|
||||
skipPlaylist := container.NewHBox(
|
||||
@@ -420,9 +420,9 @@ func (d *Diange) CreatePanel() fyne.CanvasObject {
|
||||
widget.NewLabel(source),
|
||||
widget.NewCheckWithData(i18n.T("plugin.diange.source.enable"), binding.BindBool(&cfg.Enable)),
|
||||
widget.NewLabel(i18n.T("plugin.diange.source.priority")),
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&cfg.Priority)))),
|
||||
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&cfg.Priority))),
|
||||
widget.NewLabel(i18n.T("plugin.diange.source.command")),
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.BindString(&cfg.Command))),
|
||||
widget.NewEntryWithData(binding.BindString(&cfg.Command)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package durationmgmt
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -42,7 +42,7 @@ func (d *MaxDuration) Name() string {
|
||||
|
||||
func (d *MaxDuration) Enable() error {
|
||||
config.LoadConfig(d)
|
||||
gui.AddConfigLayout(d)
|
||||
config2.AddConfigLayout(d)
|
||||
global.EventBus.Subscribe("",
|
||||
events.PlayerPropertyDurationUpdate,
|
||||
"plugin.maxduration.duration",
|
||||
@@ -91,7 +91,7 @@ func (d *MaxDuration) CreatePanel() fyne.CanvasObject {
|
||||
if d.panel != nil {
|
||||
return d.panel
|
||||
}
|
||||
maxDurationInput := xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.MaxDuration))))
|
||||
maxDurationInput := widget.NewEntryWithData(binding.IntToString(binding.BindInt(&d.MaxDuration)))
|
||||
skipOnPlayCheckbox := widget.NewCheckWithData(i18n.T("plugin.maxduration.enable"), binding.BindBool(&d.SkipOnPlay))
|
||||
skipOnReachCheckbox := widget.NewCheckWithData(i18n.T("plugin.maxduration.enable"), binding.BindBool(&d.SkipOnReach))
|
||||
d.panel = container.New(
|
||||
|
||||
@@ -3,9 +3,9 @@ package qiege
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -44,7 +44,7 @@ func (d *Qiege) Name() string {
|
||||
|
||||
func (d *Qiege) Enable() error {
|
||||
config.LoadConfig(d)
|
||||
gui.AddConfigLayout(d)
|
||||
config2.AddConfigLayout(d)
|
||||
global.EventBus.Subscribe("",
|
||||
events.LiveRoomMessageReceive,
|
||||
"plugin.qiege.message",
|
||||
@@ -121,7 +121,7 @@ func (d *Qiege) CreatePanel() fyne.CanvasObject {
|
||||
)
|
||||
qgShortCut := container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.qiege.custom_cmd")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.BindString(&d.CustomCMD))),
|
||||
widget.NewEntryWithData(binding.BindString(&d.CustomCMD)),
|
||||
)
|
||||
d.panel = container.NewVBox(dgPerm, qgShortCut)
|
||||
return d.panel
|
||||
|
||||
@@ -3,8 +3,8 @@ package sourcelogin
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/component"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
@@ -49,7 +49,7 @@ func (w *SourceLogin) Name() string {
|
||||
|
||||
func (w *SourceLogin) Enable() error {
|
||||
config.LoadConfig(w)
|
||||
gui.AddConfigLayout(w)
|
||||
config2.AddConfigLayout(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ func (w *SourceLogin) Description() string {
|
||||
return i18n.T("plugin.sourcelogin.description")
|
||||
}
|
||||
|
||||
// todo: fix using fyne async update ui
|
||||
func (w *SourceLogin) CreatePanel() fyne.CanvasObject {
|
||||
if w.panel != nil {
|
||||
return w.panel
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/component"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -92,7 +92,7 @@ func (t *TextInfo) Enable() (err error) {
|
||||
config.LoadConfig(t)
|
||||
t.reloadTemplates()
|
||||
t.registerHandlers()
|
||||
gui.AddConfigLayout(t)
|
||||
config2.AddConfigLayout(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
5
plugin/wshub/consts.go
Normal file
5
plugin/wshub/consts.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package wshub
|
||||
|
||||
const (
|
||||
eventChannel = "wshub"
|
||||
)
|
||||
@@ -49,7 +49,7 @@ func (c *wsClient) start() {
|
||||
return
|
||||
}
|
||||
if globalEnableWsHubControl {
|
||||
_ = global.EventBus.Publish(data.EventID, actualEventData)
|
||||
_ = global.EventBus.PublishToChannel(eventChannel, data.EventID, actualEventData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package wshub
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -46,7 +46,7 @@ func (w *WsHub) Enable() error {
|
||||
// todo: should pass EnableWsHubControl to client instead of using global variable
|
||||
globalEnableWsHubControl = w.EnableWsHubControl
|
||||
w.server = newWsServer(&w.Port, &w.LocalHostOnly)
|
||||
gui.AddConfigLayout(w)
|
||||
config2.AddConfigLayout(w)
|
||||
w.registerEvents()
|
||||
w.log.Info("webinfo loaded")
|
||||
if w.Enabled {
|
||||
@@ -104,9 +104,9 @@ func (w *WsHub) CreatePanel() fyne.CanvasObject {
|
||||
freshStatusText()
|
||||
serverPort := container.NewBorder(nil, nil,
|
||||
widget.NewLabel(i18n.T("plugin.wshub.port")), nil,
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.IntToString(binding.BindInt(&w.Port)))),
|
||||
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&w.Port))),
|
||||
)
|
||||
serverUrl := xfyne.EntryDisableUndoRedo(widget.NewEntry())
|
||||
serverUrl := widget.NewEntry()
|
||||
serverUrl.SetText(w.server.getWsUrl())
|
||||
serverUrl.Disable()
|
||||
serverPreview := container.NewBorder(nil, nil,
|
||||
@@ -175,7 +175,7 @@ func (w *WsHub) registerEvents() {
|
||||
for eid, _ := range events.EventsMapping {
|
||||
eventCache = append(eventCache, &EventData{})
|
||||
currentIdx := i
|
||||
global.EventBus.Subscribe("", eid,
|
||||
global.EventBus.Subscribe(eventChannel, eid,
|
||||
"plugin.wshub.event."+string(eid),
|
||||
func(e *eventbus.Event) {
|
||||
ed := EventData{
|
||||
|
||||
@@ -3,9 +3,9 @@ package yinliang
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/xfyne"
|
||||
config2 "AynaLivePlayer/gui/views/config"
|
||||
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -65,7 +65,7 @@ func (y *Yinliang) Enable() error {
|
||||
y.MaxVolume = 0
|
||||
}
|
||||
|
||||
gui.AddConfigLayout(y)
|
||||
config2.AddConfigLayout(y)
|
||||
|
||||
_ = global.EventBus.Subscribe("",
|
||||
events.LiveRoomMessageReceive,
|
||||
@@ -147,9 +147,9 @@ func (y *Yinliang) CreatePanel() fyne.CanvasObject {
|
||||
|
||||
cmdConfig := container.NewGridWithColumns(2,
|
||||
widget.NewLabel(i18n.T("plugin.yinliang.volume_up_cmd")),
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.BindString(&y.VolumeUpCMD))),
|
||||
widget.NewEntryWithData(binding.BindString(&y.VolumeUpCMD)),
|
||||
widget.NewLabel(i18n.T("plugin.yinliang.volume_down_cmd")),
|
||||
xfyne.EntryDisableUndoRedo(widget.NewEntryWithData(binding.BindString(&y.VolumeDownCMD))),
|
||||
widget.NewEntryWithData(binding.BindString(&y.VolumeDownCMD)),
|
||||
)
|
||||
|
||||
stepEntry := widget.NewEntryWithData(binding.FloatToStringWithFormat(binding.BindFloat(&y.VolumeStep), "%.1f"))
|
||||
@@ -182,9 +182,9 @@ func (y *Yinliang) CreatePanel() fyne.CanvasObject {
|
||||
|
||||
volumeControlConfig := container.NewGridWithColumns(2,
|
||||
widget.NewLabel(i18n.T("plugin.yinliang.volume_step")),
|
||||
xfyne.EntryDisableUndoRedo(stepEntry),
|
||||
stepEntry,
|
||||
widget.NewLabel(i18n.T("plugin.yinliang.max_volume")),
|
||||
xfyne.EntryDisableUndoRedo(maxVolEntry),
|
||||
maxVolEntry,
|
||||
)
|
||||
|
||||
y.panel = container.NewVBox(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package resource
|
||||
|
||||
import "fyne.io/fyne/v2"
|
||||
import (
|
||||
_ "embed"
|
||||
"fyne.io/fyne/v2"
|
||||
)
|
||||
|
||||
var ImageEmpty = &fyne.StaticResource{
|
||||
StaticName: "flat-color-icons--audio-file.svg",
|
||||
@@ -12,4 +15,10 @@ var ImageEmptyQrCode = &fyne.StaticResource{
|
||||
StaticContent: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path fill=\"black\" d=\"M1 1h10v10H1zm2 2v6h6V3z\"/><path fill=\"black\" fill-rule=\"evenodd\" d=\"M5 5h2v2H5z\"/><path fill=\"black\" d=\"M13 1h10v10H13zm2 2v6h6V3z\"/><path fill=\"black\" fill-rule=\"evenodd\" d=\"M17 5h2v2h-2z\"/><path fill=\"black\" d=\"M1 13h10v10H1zm2 2v6h6v-6z\"/><path fill=\"black\" fill-rule=\"evenodd\" d=\"M5 17h2v2H5z\"/><path fill=\"black\" d=\"M23 19h-4v4h-6V13h1h-1v6h2v2h2v-6h-2v-2h-1h3v2h2v2h2v-4h2zm0 2v2h-2v-2z\"/></svg>"),
|
||||
}
|
||||
|
||||
var ImageIcon = resImageIcon
|
||||
//go:embed static/icon2.png
|
||||
var resImageIconData []byte
|
||||
|
||||
var ImageIcon = &fyne.StaticResource{
|
||||
StaticName: "icon2.png",
|
||||
StaticContent: resImageIconData,
|
||||
}
|
||||
|
||||
BIN
resource/static/icon2.png
Normal file
BIN
resource/static/icon2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
Reference in New Issue
Block a user