mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2025-12-08 11:18:12 +08:00
gui refactor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package gui
|
||||
package component
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
40
gui/component/label.go
Normal file
40
gui/component/label.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type LabelOpt func(*widget.Label)
|
||||
|
||||
func LabelWrapping(wrapping fyne.TextWrap) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.Wrapping = wrapping
|
||||
}
|
||||
}
|
||||
|
||||
func LabelAlignment(align fyne.TextAlign) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.Alignment = align
|
||||
}
|
||||
}
|
||||
|
||||
func LabelTextStyle(style fyne.TextStyle) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.TextStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
func LabelTruncation(truncation fyne.TextTruncation) LabelOpt {
|
||||
return func(l *widget.Label) {
|
||||
l.Truncation = truncation
|
||||
}
|
||||
}
|
||||
|
||||
func NewLabelWithOpts(text string, opts ...LabelOpt) *widget.Label {
|
||||
l := widget.NewLabel(text)
|
||||
for _, opt := range opts {
|
||||
opt(l)
|
||||
}
|
||||
return l
|
||||
}
|
||||
28
gui/component/lyrics/LICENSE.txt
Normal file
28
gui/component/lyrics/LICENSE.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Drew Weymouth
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
97
gui/component/lyrics/lyricline.go
Normal file
97
gui/component/lyrics/lyricline.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package lyrics
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type lyricLine struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Text string
|
||||
SizeName fyne.ThemeSizeName
|
||||
ColorName fyne.ThemeColorName
|
||||
HoveredColorName fyne.ThemeColorName
|
||||
Alignment fyne.TextAlign
|
||||
Tappable bool
|
||||
|
||||
onTapped func()
|
||||
hovered bool
|
||||
richtext *widget.RichText
|
||||
}
|
||||
|
||||
func newLyricLine(text string, onTapped func()) *lyricLine {
|
||||
l := &lyricLine{
|
||||
Text: text,
|
||||
SizeName: theme.SizeNameSubHeadingText,
|
||||
ColorName: theme.ColorNameForeground,
|
||||
Alignment: fyne.TextAlignLeading,
|
||||
onTapped: onTapped,
|
||||
}
|
||||
l.ExtendBaseWidget(l)
|
||||
return l
|
||||
}
|
||||
|
||||
var _ desktop.Hoverable = (*lyricLine)(nil)
|
||||
|
||||
func (l *lyricLine) MouseIn(*desktop.MouseEvent) {
|
||||
if l.Tappable {
|
||||
l.hovered = true
|
||||
l.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lyricLine) MouseMoved(*desktop.MouseEvent) {
|
||||
}
|
||||
|
||||
func (l *lyricLine) MouseOut() {
|
||||
l.hovered = false
|
||||
l.Refresh()
|
||||
}
|
||||
|
||||
var _ desktop.Cursorable = (*lyricLine)(nil)
|
||||
|
||||
func (l *lyricLine) Cursor() desktop.Cursor {
|
||||
if l.Tappable {
|
||||
return desktop.PointerCursor
|
||||
}
|
||||
return desktop.DefaultCursor
|
||||
}
|
||||
|
||||
var _ fyne.Tappable = (*lyricLine)(nil)
|
||||
|
||||
func (l *lyricLine) Tapped(*fyne.PointEvent) {
|
||||
if l.Tappable {
|
||||
l.onTapped()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lyricLine) updateRichText() {
|
||||
if l.richtext == nil {
|
||||
l.richtext = widget.NewRichText(&widget.TextSegment{
|
||||
Style: widget.RichTextStyleSubHeading,
|
||||
})
|
||||
l.richtext.Wrapping = fyne.TextWrapWord
|
||||
}
|
||||
seg := l.richtext.Segments[0].(*widget.TextSegment)
|
||||
seg.Text = l.Text
|
||||
seg.Style.Alignment = l.Alignment
|
||||
if l.hovered {
|
||||
seg.Style.ColorName = l.HoveredColorName
|
||||
} else {
|
||||
seg.Style.ColorName = l.ColorName
|
||||
}
|
||||
seg.Style.SizeName = l.SizeName
|
||||
}
|
||||
|
||||
func (l *lyricLine) Refresh() {
|
||||
l.updateRichText()
|
||||
l.richtext.Refresh()
|
||||
}
|
||||
|
||||
func (l *lyricLine) CreateRenderer() fyne.WidgetRenderer {
|
||||
l.updateRichText()
|
||||
return widget.NewSimpleRenderer(l.richtext)
|
||||
}
|
||||
412
gui/component/lyrics/lyricsviewer.go
Normal file
412
gui/component/lyrics/lyricsviewer.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package lyrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type ActiveLyricPosition int
|
||||
|
||||
const (
|
||||
// ActiveLyricPositionMiddle positions the active lyric line in the middle of the widget
|
||||
ActiveLyricPositionMiddle ActiveLyricPosition = iota
|
||||
|
||||
// ActiveLyricPositionUpperMiddle positions the active lyric line
|
||||
// in the upper-middle of the widget, roughly 1/3 of the way down
|
||||
ActiveLyricPositionUpperMiddle
|
||||
)
|
||||
|
||||
// LyricsViewer is a widget for displaying song lyrics.
|
||||
// It supports synced and unsynced mode. In synced mode, the active line
|
||||
// is highlighted and the widget can advance to the next line
|
||||
// with an animated scroll. In unsynced mode all lyrics are shown
|
||||
// in the active color and the user is allowed to scroll freely.
|
||||
type LyricsViewer struct {
|
||||
widget.BaseWidget
|
||||
|
||||
// Alignment controls the text alignment of the lyric lines
|
||||
Alignment fyne.TextAlign
|
||||
|
||||
// TextSizeName is the theme size name that controls the size of the lyric lines.
|
||||
// Defaults to theme.SizeNameSubHeadingText.
|
||||
TextSizeName fyne.ThemeSizeName
|
||||
|
||||
// ActiveLyricColorName is the theme color name that the currently active
|
||||
// lyric line will be drawn in synced mode, or all lyrics in non-synced mode.
|
||||
// Defaults to theme.ColorNameForeground.
|
||||
ActiveLyricColorName fyne.ThemeColorName
|
||||
|
||||
// InactiveLyricColorName is the theme color name that the inactive lyric lines
|
||||
// will be drawn in synced mode. Defaults to theme.ColorNameDisabled.
|
||||
InactiveLyricColorName fyne.ThemeColorName
|
||||
|
||||
// HoveredLyricColorName is the theme color name that hovered lyric lines
|
||||
// will be drawn in synced mode when an OnLyricTapped callback is set.
|
||||
// Defaults to theme.ColorNameHover.
|
||||
HoveredLyricColorName fyne.ThemeColorName
|
||||
|
||||
// ActiveLyricPosition sets the vertical positioning of the active lyric line
|
||||
// in synced mode.
|
||||
ActiveLyricPosition ActiveLyricPosition
|
||||
|
||||
// OnLyricTapped sets a callback function that is invoked when a
|
||||
// synced lyric line is tapped. The line number is *one-indexed*.
|
||||
// Typically used to seek to the timecode of the given lyric.
|
||||
// When showing unsynced lyrics, or if this callback is unset,
|
||||
// the visual styling of the widget will not indicate interactivity.
|
||||
OnLyricTapped func(lineNum int)
|
||||
|
||||
lines []string
|
||||
synced bool
|
||||
|
||||
// one-indexed - 0 means before the first line
|
||||
// during an animation, currentLine is the line
|
||||
// that will be scrolled when the animation is finished
|
||||
currentLine int
|
||||
|
||||
prototypeLyricLineSize fyne.Size
|
||||
|
||||
scroll *container.Scroll
|
||||
vbox *fyne.Container
|
||||
|
||||
// nil when an animation is not currently running
|
||||
anim *fyne.Animation
|
||||
animStartOffset float32
|
||||
}
|
||||
|
||||
// NewLyricsViewer returns a new lyrics viewer.
|
||||
func NewLyricsViewer() *LyricsViewer {
|
||||
s := &LyricsViewer{}
|
||||
s.ExtendBaseWidget(s)
|
||||
s.prototypeLyricLineSize = s.newLyricLine("Hello...", 0, false).MinSize()
|
||||
return s
|
||||
}
|
||||
|
||||
// SetLyrics sets the lyrics and also resets the current line to 0 if synced.
|
||||
func (l *LyricsViewer) SetLyrics(lines []string, synced bool) {
|
||||
l.lines = lines
|
||||
l.synced = synced
|
||||
l.currentLine = 0
|
||||
if l.scroll != nil {
|
||||
if synced {
|
||||
l.scroll.Direction = container.ScrollNone
|
||||
} else {
|
||||
l.scroll.Direction = container.ScrollVerticalOnly
|
||||
}
|
||||
}
|
||||
l.updateContent()
|
||||
}
|
||||
|
||||
// SetCurrentLine sets the current line that the lyric viewer is scrolled to.
|
||||
// Argument is *one-indexed* - SetCurrentLine(0) means setting the scroll to be
|
||||
// before the first line. In unsynced mode this is a no-op. This function is
|
||||
// typically called when the user has seeked the playing song to a new position.
|
||||
func (l *LyricsViewer) SetCurrentLine(line int) {
|
||||
if line < 0 || line > len(l.lines) {
|
||||
// do not panic, just ignore invalid input
|
||||
return
|
||||
}
|
||||
if l.vbox == nil || !l.synced {
|
||||
l.currentLine = line
|
||||
return // renderer not created yet or unsynced mode
|
||||
}
|
||||
inactiveColor := l.inactiveLyricColor()
|
||||
if l.checkStopAnimation() && l.currentLine > 1 {
|
||||
// we were in the middle of animation
|
||||
// make sure prev line is right color
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine-1].(*lyricLine), inactiveColor, true)
|
||||
}
|
||||
if l.currentLine != 0 {
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine].(*lyricLine), inactiveColor, true)
|
||||
}
|
||||
l.currentLine = line
|
||||
if l.currentLine != 0 {
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine].(*lyricLine), l.activeLyricColor(), true)
|
||||
}
|
||||
l.scroll.Offset.Y = l.offsetForLine(l.currentLine)
|
||||
l.scroll.Refresh()
|
||||
}
|
||||
|
||||
// NextLine advances the lyric viewer to the next line with an animated scroll.
|
||||
// In unsynced mode this is a no-op.
|
||||
func (l *LyricsViewer) NextLine() {
|
||||
if l.vbox == nil || !l.synced {
|
||||
return // no renderer yet, or unsynced lyrics (no-op)
|
||||
}
|
||||
|
||||
if l.currentLine == len(l.lines) {
|
||||
return // already at last line
|
||||
}
|
||||
if l.checkStopAnimation() {
|
||||
// we were in the middle of animation - short-circuit it to completed
|
||||
// make sure prev and current lines are right color and scrolled to the end
|
||||
if l.currentLine > 1 {
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine-1].(*lyricLine), l.inactiveLyricColor(), true)
|
||||
}
|
||||
l.setLineColor(l.vbox.Objects[l.currentLine].(*lyricLine), l.activeLyricColor(), true)
|
||||
l.scroll.Offset.Y = l.offsetForLine(l.currentLine)
|
||||
}
|
||||
l.currentLine++
|
||||
|
||||
var prevLine, nextLine *lyricLine
|
||||
if l.currentLine > 1 {
|
||||
prevLine = l.vbox.Objects[l.currentLine-1].(*lyricLine)
|
||||
}
|
||||
if l.currentLine <= len(l.lines) {
|
||||
nextLine = l.vbox.Objects[l.currentLine].(*lyricLine)
|
||||
}
|
||||
|
||||
l.setupScrollAnimation(prevLine, nextLine)
|
||||
l.anim.Start()
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) Refresh() {
|
||||
l.updateContent()
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) MinSize() fyne.Size {
|
||||
// overridden because NoScroll will have minSize encompass the full lyrics
|
||||
// note also that leaving this to the renderer MinSize, based on the
|
||||
// VBox with RichText lines inside Scroll, may lead to race conditions
|
||||
// (https://github.com/fyne-io/fyne/issues/4890)
|
||||
minHeight := l.prototypeLyricLineSize.Height*3 + theme.Padding()*2
|
||||
return fyne.NewSize(l.prototypeLyricLineSize.Width, minHeight)
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) Resize(size fyne.Size) {
|
||||
l.updateSpacerSize(size)
|
||||
l.BaseWidget.Resize(size)
|
||||
if l.vbox == nil {
|
||||
return // renderer not created yet
|
||||
}
|
||||
if l.anim == nil {
|
||||
l.scroll.Offset = fyne.NewPos(0, l.offsetForLine(l.currentLine))
|
||||
l.scroll.Refresh()
|
||||
} else {
|
||||
// animation is running - update its reference scroll pos
|
||||
l.animStartOffset = l.offsetForLine(l.currentLine - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) updateSpacerSize(size fyne.Size) {
|
||||
if l.vbox == nil {
|
||||
return // renderer not created yet
|
||||
}
|
||||
|
||||
ht := size.Height / 2
|
||||
if l.ActiveLyricPosition == ActiveLyricPositionUpperMiddle {
|
||||
ht = size.Height / 3
|
||||
}
|
||||
|
||||
var topSpaceHeight, bottomSpaceHeight float32
|
||||
if l.synced {
|
||||
topSpaceHeight = ht + l.prototypeLyricLineSize.Height/2
|
||||
// end spacer only needs to be big enough - can't be too big
|
||||
// so use a very simple height calculation
|
||||
bottomSpaceHeight = size.Height
|
||||
}
|
||||
l.vbox.Objects[0].(*vSpace).Height = topSpaceHeight
|
||||
l.vbox.Objects[len(l.vbox.Objects)-1].(*vSpace).Height = bottomSpaceHeight
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) updateContent() {
|
||||
if l.vbox == nil {
|
||||
return // renderer not created yet
|
||||
}
|
||||
l.checkStopAnimation()
|
||||
|
||||
lnObj := len(l.vbox.Objects)
|
||||
if lnObj == 0 {
|
||||
l.vbox.Objects = append(l.vbox.Objects, NewVSpace(0), NewVSpace(0))
|
||||
lnObj = 2
|
||||
}
|
||||
l.updateSpacerSize(l.Size())
|
||||
endSpacer := l.vbox.Objects[lnObj-1]
|
||||
for i, line := range l.lines {
|
||||
lineNum := i + 1 // one-indexed
|
||||
useActiveColor := !l.synced || l.currentLine == lineNum
|
||||
if lineNum < lnObj-1 {
|
||||
rt := l.vbox.Objects[lineNum].(*lyricLine)
|
||||
if useActiveColor {
|
||||
l.setLineColor(rt, l.activeLyricColor(), false)
|
||||
} else {
|
||||
l.setLineColor(rt, l.inactiveLyricColor(), false)
|
||||
}
|
||||
l.setLineTextAndProperties(rt, line, lineNum, true)
|
||||
} else if lineNum < lnObj {
|
||||
// replacing end spacer (last element in Objects) with a new richtext
|
||||
l.vbox.Objects[lineNum] = l.newLyricLine(line, lineNum, useActiveColor)
|
||||
} else {
|
||||
// extending the Objects slice
|
||||
l.vbox.Objects = append(l.vbox.Objects, l.newLyricLine(line, lineNum, useActiveColor))
|
||||
}
|
||||
}
|
||||
for i := len(l.lines) + 1; i < lnObj; i++ {
|
||||
l.vbox.Objects[i] = nil
|
||||
}
|
||||
l.vbox.Objects = l.vbox.Objects[:len(l.lines)+1]
|
||||
l.vbox.Objects = append(l.vbox.Objects, endSpacer)
|
||||
l.vbox.Refresh()
|
||||
l.scroll.Offset.Y = l.offsetForLine(l.currentLine)
|
||||
l.scroll.Refresh()
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) setupScrollAnimation(currentLine, nextLine *lyricLine) {
|
||||
// calculate total scroll distance for the animation
|
||||
scrollDist := theme.Padding()
|
||||
if currentLine != nil {
|
||||
scrollDist += currentLine.Size().Height / 2
|
||||
} else {
|
||||
scrollDist += l.prototypeLyricLineSize.Height / 2
|
||||
}
|
||||
if nextLine != nil {
|
||||
scrollDist += nextLine.Size().Height / 2
|
||||
} else {
|
||||
scrollDist += l.prototypeLyricLineSize.Height / 2
|
||||
}
|
||||
|
||||
l.animStartOffset = l.scroll.Offset.Y
|
||||
var alreadyUpdated bool
|
||||
l.anim = fyne.NewAnimation(140*time.Millisecond, func(f float32) {
|
||||
l.scroll.Offset.Y = l.animStartOffset + f*scrollDist
|
||||
l.scroll.Refresh()
|
||||
if !alreadyUpdated && f >= 0.5 {
|
||||
if nextLine != nil {
|
||||
l.setLineColor(nextLine, l.activeLyricColor(), true)
|
||||
}
|
||||
if currentLine != nil {
|
||||
l.setLineColor(currentLine, l.inactiveLyricColor(), true)
|
||||
}
|
||||
alreadyUpdated = true
|
||||
}
|
||||
if f == 1 /*end of animation*/ {
|
||||
l.anim = nil
|
||||
}
|
||||
})
|
||||
l.anim.Curve = fyne.AnimationEaseInOut
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) offsetForLine(lineNum int /*one-indexed*/) float32 {
|
||||
if lineNum == 0 {
|
||||
return 0
|
||||
}
|
||||
pad := theme.Padding()
|
||||
offset := pad + l.prototypeLyricLineSize.Height/2
|
||||
for i := 1; i <= lineNum; i++ {
|
||||
if i > 1 {
|
||||
offset += l.vbox.Objects[i-1].MinSize().Height/2 + pad
|
||||
}
|
||||
offset += l.vbox.Objects[i].MinSize().Height / 2
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) newLyricLine(text string, lineNum int, useActiveColor bool) *lyricLine {
|
||||
ll := newLyricLine(text, nil)
|
||||
l.setLineTextAndProperties(ll, text, lineNum, false)
|
||||
ll.HoveredColorName = l.hoveredLyricColor()
|
||||
if useActiveColor {
|
||||
ll.ColorName = l.activeLyricColor()
|
||||
} else {
|
||||
ll.ColorName = l.inactiveLyricColor()
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) setLineTextAndProperties(ll *lyricLine, text string, lineNum int, refresh bool) {
|
||||
ll.Text = text
|
||||
ll.SizeName = l.textSizeName()
|
||||
ll.Alignment = l.Alignment
|
||||
ll.Tappable = l.synced && l.OnLyricTapped != nil
|
||||
ll.onTapped = func() {
|
||||
if l.OnLyricTapped != nil {
|
||||
l.OnLyricTapped(lineNum)
|
||||
}
|
||||
}
|
||||
if refresh {
|
||||
ll.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) setLineColor(ll *lyricLine, colorName fyne.ThemeColorName, refresh bool) {
|
||||
ll.ColorName = colorName
|
||||
ll.HoveredColorName = l.hoveredLyricColor()
|
||||
if refresh {
|
||||
ll.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) activeLyricColor() fyne.ThemeColorName {
|
||||
if l.ActiveLyricColorName != "" {
|
||||
return l.ActiveLyricColorName
|
||||
}
|
||||
return theme.ColorNameForeground
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) inactiveLyricColor() fyne.ThemeColorName {
|
||||
if l.InactiveLyricColorName != "" {
|
||||
return l.InactiveLyricColorName
|
||||
}
|
||||
return theme.ColorNameDisabled
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) hoveredLyricColor() fyne.ThemeColorName {
|
||||
if l.HoveredLyricColorName != "" {
|
||||
return l.HoveredLyricColorName
|
||||
}
|
||||
return theme.ColorNameHyperlink
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) textSizeName() fyne.ThemeSizeName {
|
||||
if l.TextSizeName != "" {
|
||||
return l.TextSizeName
|
||||
}
|
||||
return theme.SizeNameSubHeadingText
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) checkStopAnimation() bool {
|
||||
if l.anim != nil {
|
||||
l.anim.Stop()
|
||||
l.anim = nil
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *LyricsViewer) CreateRenderer() fyne.WidgetRenderer {
|
||||
l.vbox = container.NewVBox()
|
||||
l.scroll = container.NewScroll(l.vbox)
|
||||
if l.synced {
|
||||
l.scroll.Direction = container.ScrollNone
|
||||
} else {
|
||||
l.scroll.Direction = container.ScrollVerticalOnly
|
||||
}
|
||||
l.updateContent()
|
||||
return widget.NewSimpleRenderer(l.scroll)
|
||||
}
|
||||
|
||||
type vSpace struct {
|
||||
widget.BaseWidget
|
||||
|
||||
Height float32
|
||||
}
|
||||
|
||||
func NewVSpace(height float32) *vSpace {
|
||||
v := &vSpace{Height: height}
|
||||
v.ExtendBaseWidget(v)
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *vSpace) MinSize() fyne.Size {
|
||||
return fyne.NewSize(0, v.Height)
|
||||
}
|
||||
|
||||
func (v *vSpace) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(layout.NewSpacer())
|
||||
}
|
||||
3
gui/component/lyrics/readme.txt
Normal file
3
gui/component/lyrics/readme.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
license under bsd-3-clause
|
||||
|
||||
https://github.com/supersonic-app/fyne-lyrics
|
||||
@@ -1,5 +0,0 @@
|
||||
package gui
|
||||
|
||||
const (
|
||||
eventChannel = "gui"
|
||||
)
|
||||
43
gui/gctx/context.go
Normal file
43
gui/gctx/context.go
Normal 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)
|
||||
}
|
||||
78
gui/gui.go
78
gui/gui.go
@@ -3,7 +3,16 @@ package gui
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
configView "AynaLivePlayer/gui/views/config"
|
||||
"AynaLivePlayer/gui/views/history"
|
||||
"AynaLivePlayer/gui/views/liverooms"
|
||||
"AynaLivePlayer/gui/views/player"
|
||||
"AynaLivePlayer/gui/views/playlists"
|
||||
"AynaLivePlayer/gui/views/search"
|
||||
"AynaLivePlayer/gui/views/systray"
|
||||
"AynaLivePlayer/gui/views/updater"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -18,11 +27,6 @@ import (
|
||||
_logger "AynaLivePlayer/pkg/logger"
|
||||
)
|
||||
|
||||
var App fyne.App
|
||||
var MainWindow fyne.Window
|
||||
var playerWindow fyne.Window
|
||||
var playerWindowHandle uintptr
|
||||
|
||||
var logger _logger.ILogger = nil
|
||||
|
||||
func black_magic() {
|
||||
@@ -31,48 +35,48 @@ func black_magic() {
|
||||
|
||||
func Initialize() {
|
||||
logger = global.Logger.WithPrefix("GUI")
|
||||
gctx.Logger = logger
|
||||
black_magic()
|
||||
logger.Info("Initializing GUI")
|
||||
|
||||
if config.General.CustomFonts != "" {
|
||||
_ = os.Setenv("FYNE_FONT", config.GetAssetPath(config.General.CustomFonts))
|
||||
}
|
||||
App = app.NewWithID(config.ProgramName)
|
||||
//App.Settings().SetTheme(&myTheme{})
|
||||
MainWindow = App.NewWindow(getAppTitle())
|
||||
|
||||
mainApp := app.NewWithID(config.ProgramName)
|
||||
MainWindow := mainApp.NewWindow(getAppTitle())
|
||||
|
||||
gctx.Context = gctx.NewGuiContext(mainApp, MainWindow)
|
||||
gctx.Context.Init()
|
||||
|
||||
gctx.Context.OnMainWindowClosing(func() {
|
||||
_ = config.SaveToConfigFile(config.ConfigPath)
|
||||
logger.Infof("config saved to %s", config.ConfigPath)
|
||||
})
|
||||
|
||||
updater.CreateUpdaterPopUp()
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem(i18n.T("gui.tab.player"),
|
||||
container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.search"),
|
||||
container.NewBorder(createSearchBar(), nil, nil, nil, createSearchList()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.room"),
|
||||
container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.playlist"),
|
||||
container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.history"),
|
||||
container.NewBorder(nil, nil, nil, nil, createHistoryList()),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.config"),
|
||||
createConfigLayout(),
|
||||
),
|
||||
container.NewTabItem(i18n.T("gui.tab.player"), player.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.search"), search.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.room"), liverooms.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.playlist"), playlists.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.history"), history.CreateView()),
|
||||
container.NewTabItem(i18n.T("gui.tab.config"), configView.CreateView()),
|
||||
)
|
||||
|
||||
tabs.SetTabLocation(container.TabLocationTop)
|
||||
MainWindow.SetIcon(resource.ImageIcon)
|
||||
MainWindow.SetContent(tabs)
|
||||
//MainWindow.Resize(fyne.NewSize(1280, 720))
|
||||
MainWindow.Resize(fyne.NewSize(config.General.Width, config.General.Height))
|
||||
MainWindow.SetFixedSize(config.General.FixedSize)
|
||||
|
||||
// todo: fix, window were created even if not show. this block gui from closing
|
||||
// i can't create sub window before the main window shows.
|
||||
// setupPlayerWindow()
|
||||
|
||||
// register error
|
||||
global.EventBus.Subscribe(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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
249
gui/liverooms.go
249
gui/liverooms.go
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")),
|
||||
@@ -1,4 +1,4 @@
|
||||
package gui
|
||||
package config
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/gui/component"
|
||||
@@ -21,7 +21,7 @@ func AddConfigLayout(cfgs ...ConfigLayout) {
|
||||
ConfigList = append(ConfigList, cfgs...)
|
||||
}
|
||||
|
||||
func createConfigLayout() fyne.CanvasObject {
|
||||
func CreateView() fyne.CanvasObject {
|
||||
// initialize config panels
|
||||
for _, c := range ConfigList {
|
||||
c.CreatePanel()
|
||||
@@ -1,9 +1,11 @@
|
||||
package gui
|
||||
package history
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -15,16 +17,34 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var History = &struct {
|
||||
Medias []model.Media
|
||||
List *widget.List
|
||||
mux sync.RWMutex
|
||||
}{}
|
||||
var medias []model.Media
|
||||
var listWidget *widget.List
|
||||
var mux sync.RWMutex
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
view := createHistoryList()
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.PlayerPlayingUpdate,
|
||||
"gui.history.playing_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
|
||||
return
|
||||
}
|
||||
mux.Lock()
|
||||
medias = append(medias, event.Data.(events.PlayerPlayingUpdateEvent).Media)
|
||||
if len(medias) > 1000 {
|
||||
medias = medias[len(medias)-1000:]
|
||||
}
|
||||
listWidget.Refresh()
|
||||
mux.Unlock()
|
||||
}))
|
||||
return view
|
||||
}
|
||||
|
||||
func createHistoryList() fyne.CanvasObject {
|
||||
History.List = widget.NewList(
|
||||
listWidget = widget.NewList(
|
||||
func() int {
|
||||
return len(History.Medias)
|
||||
return len(medias)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return container.NewBorder(nil, nil,
|
||||
@@ -34,12 +54,12 @@ func createHistoryList() fyne.CanvasObject {
|
||||
widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil),
|
||||
),
|
||||
container.NewGridWithColumns(3,
|
||||
newLabelWithWrapping("title", fyne.TextTruncate),
|
||||
newLabelWithWrapping("artist", fyne.TextTruncate),
|
||||
newLabelWithWrapping("user", fyne.TextTruncate)))
|
||||
component.NewLabelWithOpts("title", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("artist", component.LabelTruncation(fyne.TextTruncateClip)),
|
||||
component.NewLabelWithOpts("user", component.LabelTruncation(fyne.TextTruncateClip))))
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
m := History.Medias[len(History.Medias)-id-1]
|
||||
m := medias[len(medias)-id-1]
|
||||
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
|
||||
m.Info.Title)
|
||||
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
|
||||
@@ -50,18 +70,17 @@ func createHistoryList() fyne.CanvasObject {
|
||||
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
|
||||
m.User = model.SystemUser
|
||||
btns[0].(*widget.Button).OnTapped = func() {
|
||||
_ = global.EventBus.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()
|
||||
}))
|
||||
}
|
||||
195
gui/views/liverooms/liverooms.go
Normal file
195
gui/views/liverooms/liverooms.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package liverooms
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
view := container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController())
|
||||
registerRoomHandlers()
|
||||
return view
|
||||
}
|
||||
|
||||
var providers []model.LiveRoomProviderInfo = make([]model.LiveRoomProviderInfo, 0)
|
||||
var rooms []model.LiveRoom = make([]model.LiveRoom, 0)
|
||||
|
||||
var lock sync.RWMutex
|
||||
|
||||
var currentRoomView = &struct {
|
||||
roomTitle *widget.Label
|
||||
roomID *widget.Label
|
||||
status *widget.Label
|
||||
autoConnect *widget.Check
|
||||
connectBtn *widget.Button
|
||||
disConnectBtn *widget.Button
|
||||
}{}
|
||||
|
||||
var currentIndex int = 0
|
||||
|
||||
func getCurrentRoom() (model.LiveRoom, bool) {
|
||||
lock.RLock()
|
||||
if currentIndex >= len(rooms) {
|
||||
lock.RUnlock()
|
||||
return model.LiveRoom{}, false
|
||||
}
|
||||
room := rooms[currentIndex]
|
||||
lock.RUnlock()
|
||||
return room, true
|
||||
}
|
||||
|
||||
func renderCurrentRoom() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
currentRoomView.roomTitle.SetText("")
|
||||
currentRoomView.roomID.SetText("")
|
||||
currentRoomView.autoConnect.SetChecked(false)
|
||||
currentRoomView.status.SetText(i18n.T("gui.room.waiting"))
|
||||
return
|
||||
}
|
||||
currentRoomView.roomTitle.SetText(room.DisplayName())
|
||||
currentRoomView.roomID.SetText(room.LiveRoom.Identifier())
|
||||
currentRoomView.autoConnect.SetChecked(room.Config.AutoConnect)
|
||||
if room.Status {
|
||||
currentRoomView.status.SetText(i18n.T("gui.room.status.connected"))
|
||||
} else {
|
||||
currentRoomView.status.SetText(i18n.T("gui.room.status.disconnected"))
|
||||
}
|
||||
}
|
||||
|
||||
func registerRoomHandlers() {
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.LiveRoomProviderUpdate,
|
||||
"gui.liveroom.provider_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
providers = event.Data.(events.LiveRoomProviderUpdateEvent).Providers
|
||||
//RoomTab.Rooms.Refresh()
|
||||
}))
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.UpdateLiveRoomRooms,
|
||||
"gui.liveroom.rooms_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
gctx.Logger.Infof("Update rooms")
|
||||
lock.Lock()
|
||||
rooms = event.Data.(events.UpdateLiveRoomRoomsData).Rooms
|
||||
lock.Unlock()
|
||||
renderRoomList()
|
||||
renderCurrentRoom()
|
||||
}))
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.UpdateLiveRoomStatus,
|
||||
"gui.liveroom.room_status_update",
|
||||
gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
room := event.Data.(events.UpdateLiveRoomStatusData).Room
|
||||
lock.Lock()
|
||||
index := -1
|
||||
for i := 0; i < len(rooms); i++ {
|
||||
if rooms[i].LiveRoom.Identifier() == room.LiveRoom.Identifier() {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
lock.Unlock()
|
||||
return
|
||||
}
|
||||
rooms[index] = room
|
||||
lock.Unlock()
|
||||
if index == currentIndex {
|
||||
renderCurrentRoom()
|
||||
}
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
func createRoomController() fyne.CanvasObject {
|
||||
currentRoomView.connectBtn = widget.NewButton(i18n.T("gui.room.btn.connect"), func() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("Connect to room %s", room.LiveRoom.Identifier())
|
||||
currentRoomView.connectBtn.Disable()
|
||||
go func() {
|
||||
resp, err := global.EventBus.Call(events.CmdLiveRoomOperation, events.ReplyLiveRoomOperation, events.CmdLiveRoomOperationData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
SetConnect: true,
|
||||
})
|
||||
if err != nil {
|
||||
gctx.Logger.Errorf("failed to connect to room %s", room.LiveRoom.Identifier())
|
||||
gutil.RunInFyneThread(currentRoomView.connectBtn.Enable)
|
||||
return
|
||||
}
|
||||
if resp.Data.(events.ReplyLiveRoomOperationData).Err != nil {
|
||||
err = resp.Data.(events.ReplyLiveRoomOperationData).Err
|
||||
}
|
||||
if err != nil {
|
||||
// todo: show error
|
||||
}
|
||||
gutil.RunInFyneThread(currentRoomView.connectBtn.Enable)
|
||||
}()
|
||||
})
|
||||
currentRoomView.disConnectBtn = widget.NewButton(i18n.T("gui.room.btn.disconnect"), func() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("disconnect to room %s", room.LiveRoom.Identifier())
|
||||
currentRoomView.disConnectBtn.Disable()
|
||||
go func() {
|
||||
resp, err := global.EventBus.Call(events.CmdLiveRoomOperation, events.ReplyLiveRoomOperation, events.CmdLiveRoomOperationData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
SetConnect: false,
|
||||
})
|
||||
if err != nil {
|
||||
gctx.Logger.Errorf("failed to disconnect to room %s", room.LiveRoom.Identifier())
|
||||
gutil.RunInFyneThread(currentRoomView.disConnectBtn.Enable)
|
||||
return
|
||||
}
|
||||
if resp.Data.(events.ReplyLiveRoomOperationData).Err != nil {
|
||||
err = resp.Data.(events.ReplyLiveRoomOperationData).Err
|
||||
}
|
||||
if err != nil {
|
||||
// todo: show error
|
||||
}
|
||||
gutil.RunInFyneThread(currentRoomView.disConnectBtn.Enable)
|
||||
}()
|
||||
})
|
||||
|
||||
currentRoomView.status = widget.NewLabel(i18n.T("gui.room.waiting"))
|
||||
currentRoomView.roomTitle = widget.NewLabel("")
|
||||
currentRoomView.roomID = widget.NewLabel("")
|
||||
|
||||
currentRoomView.autoConnect = widget.NewCheck(i18n.T("gui.room.check.autoconnect"), func(b bool) {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("Change room %s autoconnect to %v", room.LiveRoom.Identifier(), b)
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.CmdLiveRoomConfigChange,
|
||||
events.CmdLiveRoomConfigChangeData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
Config: model.LiveRoomConfig{
|
||||
AutoConnect: b,
|
||||
},
|
||||
})
|
||||
return
|
||||
})
|
||||
return container.NewVBox(
|
||||
currentRoomView.roomTitle,
|
||||
currentRoomView.roomID,
|
||||
currentRoomView.status,
|
||||
container.NewHBox(widget.NewLabel(i18n.T("gui.room.check.autoconnect")), currentRoomView.autoConnect),
|
||||
container.NewHBox(currentRoomView.connectBtn, currentRoomView.disConnectBtn),
|
||||
)
|
||||
}
|
||||
120
gui/views/liverooms/selector.go
Normal file
120
gui/views/liverooms/selector.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package liverooms
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var roomSelectorView = &struct {
|
||||
rooms *widget.List
|
||||
addBtn *widget.Button
|
||||
removeBtn *widget.Button
|
||||
}{}
|
||||
|
||||
func renderRoomList() {
|
||||
lock.Lock()
|
||||
roomSelectorView.rooms.Refresh()
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
func createRoomSelector() fyne.CanvasObject {
|
||||
roomSelectorView.rooms = widget.NewList(
|
||||
func() int {
|
||||
return len(rooms)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
return widget.NewLabel("AAAAAAAAAAAAAAAA")
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
object.(*widget.Label).SetText(
|
||||
rooms[id].DisplayName())
|
||||
})
|
||||
roomSelectorView.addBtn = widget.NewButton(i18n.T("gui.room.button.add"), func() {
|
||||
providerNames := make([]string, len(providers))
|
||||
for i := 0; i < len(providers); i++ {
|
||||
providerNames[i] = providers[i].Name
|
||||
}
|
||||
descriptionLabel := widget.NewLabel(i18n.T("gui.room.add.prompt"))
|
||||
clientNameEntry := widget.NewSelect(providerNames, func(s string) {
|
||||
for i := 0; i < len(providers); i++ {
|
||||
if providers[i].Name == s {
|
||||
descriptionLabel.SetText(i18n.T(providers[i].Description))
|
||||
break
|
||||
}
|
||||
descriptionLabel.SetText("")
|
||||
}
|
||||
})
|
||||
idEntry := widget.NewEntry()
|
||||
nameEntry := widget.NewEntry()
|
||||
dia := dialog.NewCustomConfirm(
|
||||
i18n.T("gui.room.add.title"),
|
||||
i18n.T("gui.room.add.confirm"),
|
||||
i18n.T("gui.room.add.cancel"),
|
||||
container.NewVBox(
|
||||
container.New(
|
||||
layout.NewFormLayout(),
|
||||
widget.NewLabel(i18n.T("gui.room.add.name")),
|
||||
nameEntry,
|
||||
widget.NewLabel(i18n.T("gui.room.add.client_name")),
|
||||
clientNameEntry,
|
||||
widget.NewLabel(i18n.T("gui.room.add.id_url")),
|
||||
idEntry,
|
||||
),
|
||||
descriptionLabel,
|
||||
),
|
||||
func(b bool) {
|
||||
if b && len(clientNameEntry.Selected) > 0 && len(idEntry.Text) > 0 {
|
||||
gctx.Logger.Infof("Add room %s %s", clientNameEntry.Selected, idEntry.Text)
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.CmdLiveRoomAdd,
|
||||
events.CmdLiveRoomAddData{
|
||||
Title: nameEntry.Text,
|
||||
Provider: clientNameEntry.Selected,
|
||||
RoomKey: idEntry.Text,
|
||||
})
|
||||
}
|
||||
},
|
||||
gctx.Context.Window,
|
||||
)
|
||||
dia.Resize(fyne.NewSize(512, 256))
|
||||
dia.Show()
|
||||
})
|
||||
roomSelectorView.removeBtn = widget.NewButton(i18n.T("gui.room.button.remove"), func() {
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel,
|
||||
events.CmdLiveRoomRemove,
|
||||
events.CmdLiveRoomRemoveData{
|
||||
Identifier: room.LiveRoom.Identifier(),
|
||||
})
|
||||
})
|
||||
roomSelectorView.rooms.OnSelected = func(id widget.ListItemID) {
|
||||
if id >= len(rooms) {
|
||||
return
|
||||
}
|
||||
currentIndex = id
|
||||
room, ok := getCurrentRoom()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
gctx.Logger.Infof("Select room %s", room.LiveRoom.Identifier())
|
||||
renderCurrentRoom()
|
||||
}
|
||||
return container.NewHBox(
|
||||
container.NewBorder(
|
||||
nil, container.NewCenter(container.NewHBox(roomSelectorView.addBtn, roomSelectorView.removeBtn)),
|
||||
nil, nil,
|
||||
roomSelectorView.rooms,
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package gui
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -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
|
||||
@@ -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
98
gui/views/player/lyric.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component/lyrics"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"github.com/AynaLivePlayer/miaosic"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var lyricWindow fyne.Window = nil
|
||||
var lyricViewer *lyrics.LyricsViewer = nil
|
||||
var currLyrics []string
|
||||
var currentLrcObj miaosic.Lyrics = miaosic.Lyrics{}
|
||||
var lrcmux sync.RWMutex
|
||||
|
||||
func setupLyricViewer() {
|
||||
if lyricWindow != nil {
|
||||
return
|
||||
}
|
||||
lyricViewer = lyrics.NewLyricsViewer()
|
||||
lyricViewer.ActiveLyricPosition = lyrics.ActiveLyricPositionUpperMiddle
|
||||
lyricViewer.Alignment = fyne.TextAlignCenter
|
||||
lyricViewer.HoveredLyricColorName = theme.ColorNameDisabled
|
||||
lyricViewer.SetLyrics([]string{""}, true)
|
||||
lyricViewer.OnLyricTapped = func(lineNum int) {
|
||||
lineNum = lineNum - 1
|
||||
if lineNum < 0 {
|
||||
return
|
||||
}
|
||||
lrcmux.Lock()
|
||||
if lineNum >= len(currentLrcObj.Content) {
|
||||
lrcmux.Unlock()
|
||||
return
|
||||
}
|
||||
line := currentLrcObj.Content[lineNum]
|
||||
lrcmux.Unlock()
|
||||
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: line.Time,
|
||||
Absolute: true,
|
||||
})
|
||||
}
|
||||
|
||||
global.EventBus.Subscribe(gctx.EventChannel, events.UpdateCurrentLyric, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
e := event.Data.(events.UpdateCurrentLyricData)
|
||||
tmpLyric := make([]string, 0)
|
||||
for _, l := range e.Lyrics.Content {
|
||||
tmpLyric = append(tmpLyric, l.Lyric)
|
||||
}
|
||||
// ensure at least one line
|
||||
if len(tmpLyric) == 0 {
|
||||
tmpLyric = append(tmpLyric, "")
|
||||
}
|
||||
lrcmux.Lock()
|
||||
currentLrcObj = event.Data.(events.UpdateCurrentLyricData).Lyrics
|
||||
currLyrics = tmpLyric
|
||||
lyricViewer.SetLyrics(currLyrics, true)
|
||||
lyricViewer.SetCurrentLine(0)
|
||||
lrcmux.Unlock()
|
||||
}))
|
||||
|
||||
// register handlers
|
||||
global.EventBus.Subscribe(gctx.EventChannel,
|
||||
events.PlayerLyricPosUpdate, "player.lyric.lyric_pos_update", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
|
||||
e := event.Data.(events.PlayerLyricPosUpdateEvent)
|
||||
gctx.Logger.Debug("lyric update", e)
|
||||
lrcmux.Lock()
|
||||
if e.CurrentIndex >= len(currLyrics) {
|
||||
// fix race condition
|
||||
lrcmux.Unlock()
|
||||
return
|
||||
}
|
||||
index := 0
|
||||
if e.CurrentIndex != -1 {
|
||||
index = e.CurrentIndex
|
||||
}
|
||||
lyricViewer.SetCurrentLine(index + 1)
|
||||
lrcmux.Unlock()
|
||||
}))
|
||||
}
|
||||
|
||||
func createLyricWindowV2() fyne.Window {
|
||||
// create widgets
|
||||
lyricWindow = gctx.Context.App.NewWindow(i18n.T("gui.lyric.title"))
|
||||
lyricWindow.SetContent(lyricViewer)
|
||||
lyricWindow.Resize(fyne.NewSize(360, 540))
|
||||
lyricWindow.CenterOnScreen()
|
||||
lyricWindow.SetOnClosed(func() {
|
||||
PlayController.LrcWindowOpen = false
|
||||
})
|
||||
return lyricWindow
|
||||
}
|
||||
19
gui/views/player/player.go
Normal file
19
gui/views/player/player.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
setupLyricViewer()
|
||||
registerHandlers()
|
||||
gctx.Context.OnMainWindowClosing(func() {
|
||||
if playerWindow != nil {
|
||||
gctx.Logger.Infof("closing player window")
|
||||
playerWindow.Close()
|
||||
}
|
||||
})
|
||||
return container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist())
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package gui
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -28,12 +30,12 @@ func (b *playlistOperationButton) Tapped(e *fyne.PointEvent) {
|
||||
func newPlaylistOperationButton() *playlistOperationButton {
|
||||
b := &playlistOperationButton{Index: 0}
|
||||
deleteItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.delete"), func() {
|
||||
_ = global.EventBus.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()
|
||||
@@ -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})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
10
gui/views/search/search.go
Normal file
10
gui/views/search/search.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
|
||||
func CreateView() fyne.CanvasObject {
|
||||
return container.NewBorder(createSearchBar(), nil, nil, nil, createSearchList())
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package gui
|
||||
package search
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/gui/component"
|
||||
"AynaLivePlayer/gui/gctx"
|
||||
"AynaLivePlayer/gui/gutil"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/i18n"
|
||||
@@ -30,18 +31,18 @@ func createSearchBar() fyne.CanvasObject {
|
||||
SearchBar.Button = widget.NewButton(i18n.T("gui.search.search"), func() {
|
||||
keyword := SearchBar.Input.Text
|
||||
pr := SearchBar.UseSource.Selected
|
||||
logger.Debugf("Search keyword: %s, provider: %s", keyword, pr)
|
||||
gctx.Logger.Debugf("Search keyword: %s, provider: %s", keyword, pr)
|
||||
SearchResult.mux.Lock()
|
||||
SearchResult.Items = make([]model.Media, 0)
|
||||
SearchResult.List.Refresh()
|
||||
SearchResult.mux.Unlock()
|
||||
_ = global.EventBus.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,
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user