gui refactor

This commit is contained in:
aynakeya
2025-10-06 23:52:10 +08:00
parent 7c3f8587f6
commit 5c508b9664
61 changed files with 1448 additions and 618 deletions

View File

@@ -1,9 +1,11 @@
package main
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/gctx"
"AynaLivePlayer/internal"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
@@ -41,7 +43,7 @@ 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)
}
@@ -78,13 +80,13 @@ func main() {
<-quit
} else {
gui.Initialize()
gui.MainWindow.ShowAndRun()
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

@@ -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,55 +5,56 @@ import (
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
)
const LiveRoomAddCmd = "cmd.liveroom.add"
const CmdLiveRoomAdd = "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{},
@@ -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)
}

30
core/events/miaosic.go Normal file
View File

@@ -0,0 +1,30 @@
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
}
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
}

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

View File

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

43
gui/gctx/context.go Normal file
View File

@@ -0,0 +1,43 @@
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 _, f := range c.onMainWindowClosing {
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(eventChannel,
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

@@ -7,6 +7,8 @@ import (
)
func GetWindowHandle(window fyne.Window) uintptr {
// macos doesn't support --wid. :(
return 0
glfwWindow := getGlfwWindow(window)
if glfwWindow == nil {
return 0

View File

@@ -1,249 +0,0 @@
package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"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 := 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 {
logger.Infof("Add room %s %s", clientNameEntry.Selected, idEntry.Text)
_ = global.EventBus.PublishToChannel(eventChannel,
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.PublishToChannel(eventChannel,
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(eventChannel,
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(eventChannel,
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(eventChannel,
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.PublishToChannel(eventChannel,
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.PublishToChannel(eventChannel,
events.LiveRoomOperationCmd,
events.LiveRoomOperationCmdEvent{
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
SetConnect: false,
})
})
global.EventBus.Subscribe(eventChannel,
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.PublishToChannel(eventChannel,
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(eventChannel,
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(eventChannel, events.UpdateCurrentLyric, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
e := event.Data.(events.UpdateCurrentLyricData)
fullLrc.Objects = createLyricObj(&e.Lyrics)
lrcWindow.Refresh()
}))
_ = global.EventBus.PublishToChannel(eventChannel, events.CmdGetCurrentLyric, events.CmdGetCurrentLyricData{})
w.SetOnClosed(func() {
global.EventBus.Unsubscribe(events.UpdateCurrentLyric, "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.PublishToChannel(eventChannel, 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(eventChannel, 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.PublishToChannel(eventChannel, events.PlaylistModeChangeCmd(model.PlaylistIDSystem),
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistModeChangeCmd(model.PlaylistIDSystem),
events.PlaylistModeChangeCmdEvent{
Mode: mode,
})
})
global.EventBus.Subscribe(eventChannel, 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.PublishToChannel(eventChannel, events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{
Device: name,
})
})
global.EventBus.Subscribe(eventChannel,
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.PublishToChannel(eventChannel, 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.PublishToChannel(eventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
Media: m,
})
}
btns[1].(*widget.Button).OnTapped = func() {
_ = global.EventBus.PublishToChannel(eventChannel, 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(eventChannel,
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"
@@ -45,22 +46,22 @@ var PlayController = &PlayControllerContainer{}
func registerPlayControllerHandler() {
PlayController.ButtonPrev.OnTapped = func() {
_ = global.EventBus.PublishToChannel(eventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: 0,
Absolute: true,
})
}
PlayController.ButtonSwitch.OnTapped = func() {
_ = global.EventBus.PublishToChannel(eventChannel, events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
}
PlayController.ButtonNext.OnTapped = func() {
_ = global.EventBus.PublishToChannel(eventChannel, 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()
}
}
@@ -68,7 +69,7 @@ func registerPlayControllerHandler() {
showPlayerWindow()
}
global.EventBus.Subscribe(eventChannel, 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 +78,15 @@ func registerPlayControllerHandler() {
PlayController.ButtonSwitch.Refresh()
}))
global.EventBus.Subscribe(eventChannel, 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(eventChannel, 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 +102,33 @@ func registerPlayControllerHandler() {
PlayController.Progress.Max = 0
PlayController.Progress.OnDragEnd = func(f float64) {
_ = global.EventBus.PublishToChannel(eventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: f / 10,
Absolute: false,
})
}
global.EventBus.Subscribe(eventChannel, 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(eventChannel, 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(eventChannel, 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.PublishToChannel(eventChannel, 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(eventChannel, 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 +164,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,12 +220,12 @@ 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 = widget.NewLabel("00:00")
PlayController.TotalTime = widget.NewLabel("00:00")
progressItem := container.NewBorder(nil, nil,
PlayController.CurrentTime,
nil,
PlayController.TotalTime,
PlayController.Progress)
component.NewFixedHSplitContainer(PlayController.CurrentTime, PlayController.Progress, 0.1))
PlayController.Title = widget.NewLabel("Title")
PlayController.Title.Truncation = fyne.TextTruncateClip

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(eventChannel, 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")
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.PublishToChannel(eventChannel, 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.PublishToChannel(eventChannel, 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(eventChannel, 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/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()
@@ -23,9 +27,9 @@ func showPlayerWindow() {
playerWindow.Show()
if playerWindowHandle == 0 {
playerWindowHandle = gutil.GetWindowHandle(playerWindow)
logger.Infof("video output window handle: %d", playerWindowHandle)
gctx.Logger.Infof("video output window handle: %d", playerWindowHandle)
if playerWindowHandle != 0 {
_ = global.EventBus.PublishToChannel(eventChannel, events.PlayerVideoPlayerSetWindowHandleCmd,
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerVideoPlayerSetWindowHandleCmd,
events.PlayerVideoPlayerSetWindowHandleCmdEvent{Handle: playerWindowHandle})
}
}

View File

@@ -1,9 +1,11 @@
package gui
package playlists
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gctx"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/eventbus"
@@ -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 {
@@ -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.PublishToChannel(eventChannel,
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.PublishToChannel(eventChannel,
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.PublishToChannel(eventChannel, events.PlaylistManagerGetCurrentCmd, events.PlaylistManagerGetCurrentCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistManagerGetCurrentCmd, events.PlaylistManagerGetCurrentCmdEvent{
PlaylistID: PlaylistManager.currentPlaylists[id].Meta.ID(),
})
}
global.EventBus.Subscribe(eventChannel, 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(eventChannel, 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(eventChannel, 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.PublishToChannel(eventChannel, 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.PublishToChannel(eventChannel, 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.PublishToChannel(eventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
Media: m,
})
}
btns[1].(*widget.Button).OnTapped = func() {
_ = global.EventBus.PublishToChannel(eventChannel, events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
Media: m,
Position: -1,
})
}
})
global.EventBus.Subscribe(eventChannel, 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.PublishToChannel(eventChannel, events.SearchCmd, events.SearchCmdEvent{
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.CmdMiaosicSearch, events.CmdMiaosicSearchData{
Keyword: keyword,
Provider: pr,
})
})
global.EventBus.Subscribe(eventChannel, 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.PublishToChannel(eventChannel, 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.PublishToChannel(eventChannel, 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(eventChannel, 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)
}
MainWindow.SetCloseIntercept(func() {
_ = config.SaveToConfigFile(config.ConfigPath)
MainWindow.Hide()
gctx.Context.Window.SetCloseIntercept(func() {
gctx.Context.Window.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(eventChannel,
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

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

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

View File

@@ -37,6 +37,7 @@ func Initialize() {
loadMediaProvider()
handleSearch()
handleInfo()
createLyricLoader()
_ = global.EventBus.Publish(

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

View File

@@ -10,8 +10,8 @@ import (
func handleSearch() {
err := global.EventBus.Subscribe("",
events.SearchCmd, "internal.media_provider.search_handler", func(event *eventbus.Event) {
data := event.Data.(events.SearchCmdEvent)
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,8 +26,8 @@ func handleSearch() {
}
}
_ = global.EventBus.Reply(
event, events.SearchResultUpdate,
events.SearchResultUpdateEvent{
event, events.ReplyMiaosicSearch,
events.ReplyMiaosicSearchData{
Medias: medias,
})
})

View File

@@ -16,8 +16,10 @@ 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()
}
}

View File

@@ -152,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
}
@@ -328,7 +323,7 @@ func (b *bus) Call(eventId string, subEvtId string, data interface{}) (*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")
}
}

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

View File

@@ -3,7 +3,7 @@ package durationmgmt
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
config2 "AynaLivePlayer/gui/views/config"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
@@ -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",

View File

@@ -3,8 +3,8 @@ package qiege
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/component"
config2 "AynaLivePlayer/gui/views/config"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
@@ -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",

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
}

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
}

View File

@@ -3,8 +3,8 @@ package wshub
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/component"
config2 "AynaLivePlayer/gui/views/config"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
@@ -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 {

View File

@@ -3,8 +3,8 @@ package yinliang
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/component"
config2 "AynaLivePlayer/gui/views/config"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
@@ -65,7 +65,7 @@ func (y *Yinliang) Enable() error {
y.MaxVolume = 0
}
gui.AddConfigLayout(y)
config2.AddConfigLayout(y)
_ = global.EventBus.Subscribe("",
events.LiveRoomMessageReceive,