13 Commits

Author SHA1 Message Date
Aynakeya
abaa0a9d5c Merge pull request #60 from AynaLivePlayer/dev
update workflow
2025-11-06 01:26:32 +08:00
aynakeya
d06ee8f61e update workflow 2025-11-06 01:25:37 +08:00
Aynakeya
5b0c7ae5f2 Merge pull request #59 from AynaLivePlayer/dev
using embed to embed static resource
2025-11-06 01:21:56 +08:00
aynakeya
135c022cec using embed to embed static resource 2025-11-06 01:21:16 +08:00
Aynakeya
8b643cd004 Merge pull request #58 from AynaLivePlayer/dev
Dev
2025-11-06 01:06:32 +08:00
aynakeya
3c8c8f3834 try move start after gui initialized 2025-11-06 01:05:52 +08:00
aynakeya
f070ee3f47 update miaosic 2025-11-06 00:08:46 +08:00
aynakeya
f59aebd2f8 add macos system media control 2025-10-19 01:12:19 +08:00
aynakeya
650da87f64 fix bili-video playlist 2025-10-08 21:14:29 +08:00
aynakeya
2838a02c83 update fixedSize 2025-10-07 00:30:54 +08:00
aynakeya
5c508b9664 gui refactor 2025-10-06 23:52:10 +08:00
aynakeya
7c3f8587f6 fix deadlock situation 2025-10-03 23:57:17 +08:00
aynakeya
918e2e81b3 migrate to eventbus, add support to macos 2025-10-02 21:57:45 +08:00
88 changed files with 2080 additions and 867 deletions

View File

@@ -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

View File

@@ -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()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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": "音量增加命令"
}
}
}

View File

@@ -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
View 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'
*/

View File

@@ -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"

View File

@@ -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{}

View File

@@ -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
View 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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View 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(),
}
}

View File

@@ -1,4 +1,4 @@
package gui
package component
import (
"fyne.io/fyne/v2"

40
gui/component/label.go Normal file
View 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
}

View 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.

View 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)
}

View 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())
}

View File

@@ -0,0 +1,3 @@
license under bsd-3-clause
https://github.com/supersonic-app/fyne-lyrics

44
gui/gctx/context.go Normal file
View 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)
}

View File

@@ -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()
}
})
}

View File

@@ -1,6 +1,6 @@
//go:build darwin || windows || linux
package xfyne
package gutil
import (
"fyne.io/fyne/v2"

View File

@@ -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

View File

@@ -1,7 +1,6 @@
//go:build linux
// +build linux
package xfyne
package gutil
import (
"fyne.io/fyne/v2"

View File

@@ -1,7 +1,6 @@
//go:build !darwin && !windows && !linux
// +build !darwin,!windows,!linux
package xfyne
package gutil
import "fyne.io/fyne/v2"

View File

@@ -1,7 +1,6 @@
//go:build windows
// +build windows
package xfyne
package gutil
import (
"fyne.io/fyne/v2"

View File

@@ -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),
)
}

View File

@@ -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
}

View File

@@ -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")),

View File

@@ -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()

View File

@@ -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()
}))
}

View 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),
)
}

View 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(),
)
}

View File

@@ -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")

View File

@@ -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
View 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
}

View 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())
}

View File

@@ -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()

View File

@@ -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})
}
}

View File

@@ -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()

View 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())
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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()
})
}

View File

@@ -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)
}
}))
}

View File

@@ -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
}

View File

@@ -1,11 +0,0 @@
package xfyne
import (
"fyne.io/fyne/v2/widget"
"testing"
)
func TestEntryDisableUndoRedo(t *testing.T) {
entry := widget.NewEntry()
EntryDisableUndoRedo(entry)
}

View File

@@ -10,8 +10,6 @@ import (
)
func Initialize() {
handleSearch()
createLyricLoader()
handlePlayNext()
}

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -0,0 +1,5 @@
# player
**player implementation**
for now, just use mpv, vlc not fully supported yet.

View File

@@ -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()
}

View File

@@ -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
View 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(),
})
}

View File

@@ -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
View 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
View 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)
}
}

View File

@@ -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)
}
}

View File

@@ -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(),
})
}

View File

@@ -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)
}
}

View File

@@ -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(),
})
}

View 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")
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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)),
),
)
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,5 @@
package wshub
const (
eventChannel = "wshub"
)

View File

@@ -49,7 +49,7 @@ func (c *wsClient) start() {
return
}
if globalEnableWsHubControl {
_ = global.EventBus.Publish(data.EventID, actualEventData)
_ = global.EventBus.PublishToChannel(eventChannel, data.EventID, actualEventData)
}
}
}

View File

@@ -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{

View File

@@ -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(

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

View File

@@ -16,6 +16,7 @@
----
Finished
- 2024.09.30 : qq音乐微信登陆
- 2024.08.07 : 修复由于无法获取到连接的歌曲导致的播放的歌曲和展示的信息不匹配的问题/修复qq登陆刷新logic
- 2024.07.30 : 由于相关法律法规问题,删除了第三方歌源
- 2024.07.24 : 修复网易云修复wshub大小写问题