Add GTK application implementation
This commit is contained in:
parent
d1bb505e0a
commit
b7c0307f6b
29 changed files with 2564 additions and 266 deletions
32
data/data.go
Normal file
32
data/data.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed ui/window.ui
|
||||
var WindowXML string
|
||||
|
||||
//go:embed ui/shortcuts-dialog.ui
|
||||
var ShortcutsDialogXML string
|
||||
|
||||
//go:embed ui/connection-panel.ui
|
||||
var ConnectionPanelXML string
|
||||
|
||||
//go:embed ui/server-panel.ui
|
||||
var ServerPanelXML string
|
||||
|
||||
//go:embed ui/cover-panel.ui
|
||||
var CoverPanelXML string
|
||||
|
||||
//go:embed ui/playlist-panel.ui
|
||||
var PlaylistPanelXML string
|
||||
|
||||
//go:embed ui/album-headerbar.ui
|
||||
var AlbumHeaderbarXML string
|
||||
|
||||
//go:embed ui/library-panel.ui
|
||||
var LibraryPanelXML string
|
||||
|
||||
//go:embed gtk.css
|
||||
var MainCSS string
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
#content_stack {
|
||||
box-shadow:inset 4px 4px 10px rgba(0,0,0,0.3);
|
||||
background-image:url('noise-texture.png');
|
||||
}
|
||||
|
||||
#port_spinner {
|
||||
|
|
@ -30,10 +29,6 @@
|
|||
font-weight:bold;
|
||||
}
|
||||
|
||||
window.fullscreen #cover_box {
|
||||
background: black;
|
||||
}
|
||||
|
||||
#cover_info_revealer {
|
||||
background-color:alpha(@theme_bg_color, 0.8);
|
||||
box-shadow:0 0 10px @theme_bg_color;
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
application_id = 'xyz.suruatoel.mcg'
|
||||
|
||||
scalable_dir = join_paths('hicolor', 'scalable', 'apps')
|
||||
install_data(
|
||||
join_paths(scalable_dir, ('@0@.svg').format(application_id)),
|
||||
install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir)
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<interface>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<template class="McgAlbumHeaderbar" parent="AdwBin">
|
||||
<object id="McgAlbumHeaderbar" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkHeaderBar">
|
||||
<child type="title">
|
||||
|
|
@ -25,9 +25,8 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<object id="close_button" class="GtkButton">
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_close_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
|
|
@ -37,5 +36,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -9,79 +9,58 @@
|
|||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">100</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="toolbar">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">6</property>
|
||||
</object>
|
||||
<template class="McgConnectionPanel" parent="AdwBin">
|
||||
<object id="McgConnectionPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title">Connect to MPD</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<object class="GtkListBox">
|
||||
<property name="hexpand">false</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="selection-mode">none</property>
|
||||
<style>
|
||||
<class name="boxed-list"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkListBox" id="zeroconf_list">
|
||||
<property name="hexpand">true</property>
|
||||
<child type="placeholder">
|
||||
<object class="AdwEntryRow" id="host_row">
|
||||
<property name="title" translatable="yes">Host</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="header"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">No service found</property>
|
||||
<property name="label" translatable="yes">Port</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">false</property>
|
||||
<style>
|
||||
<class name="subtitle"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="port_spinner">
|
||||
<property name="name">port_spinner</property>
|
||||
<property name="value">6600</property>
|
||||
<property name="adjustment">server-port-adjustment</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkListBox">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="selection-mode">none</property>
|
||||
<style>
|
||||
<class name="boxed-list"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwEntryRow" id="host_row">
|
||||
<property name="title" translatable="yes">Host</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
<signal name="apply" handler="on_host_entry_apply"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="header"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Port</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">false</property>
|
||||
<style>
|
||||
<class name="subtitle"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="port_spinner">
|
||||
<property name="name">port_spinner</property>
|
||||
<property name="value">6600</property>
|
||||
<property name="adjustment">server-port-adjustment</property>
|
||||
<signal name="value-changed" handler="on_port_spinner_value_changed"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPasswordEntryRow" id="password_row">
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
</object>
|
||||
</child>
|
||||
<object class="AdwPasswordEntryRow" id="password_row">
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<template class="McgCoverPanel" parent="GtkOverlay">
|
||||
<object id="McgCoverPanel" class="GtkOverlay">
|
||||
<child>
|
||||
<object class="GtkStack" id="cover_stack">
|
||||
<property name="visible-child">cover_default</property>
|
||||
|
|
@ -157,5 +157,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,11 @@
|
|||
<property name="round-digits">0</property>
|
||||
<property name="digits">0</property>
|
||||
<property name="has-origin">False</property>
|
||||
<signal name="value-changed" handler="on_grid_scale_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="library-toolbar-update">
|
||||
<property name="label" translatable="yes">update library</property>
|
||||
<signal name="clicked" handler="on_update_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -53,7 +51,6 @@
|
|||
<property name="label" translatable="yes">sort by artist</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="group">sort_year</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -61,14 +58,12 @@
|
|||
<property name="label" translatable="yes">sort by title</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="group">sort_year</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="sort_year">
|
||||
<property name="label" translatable="yes">sort by year</property>
|
||||
<property name="receives-default">False</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -76,7 +71,6 @@
|
|||
<property name="label" translatable="yes">sort by modification</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="group">sort_year</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -86,7 +80,6 @@
|
|||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<signal name="toggled" handler="on_sort_order_toggled" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -108,9 +101,6 @@
|
|||
<property name="icon-name">system-search-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<!--
|
||||
<accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
-->
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
|
@ -119,7 +109,6 @@
|
|||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Select multiple albums</property>
|
||||
<signal name="toggled" handler="on_select_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
|
|
@ -145,7 +134,7 @@
|
|||
</child>
|
||||
</object>
|
||||
|
||||
<template class="McgLibraryPanel" parent="AdwBin">
|
||||
<object id="McgLibraryPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkStack" id="library_stack">
|
||||
<property name="transition-type">slide-left-right</property>
|
||||
|
|
@ -158,7 +147,6 @@
|
|||
<child>
|
||||
<object class="GtkSearchEntry" id="filter_entry">
|
||||
<property name="placeholder-text" translatable="yes">search library</property>
|
||||
<signal name="search-changed" handler="on_filter_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -206,49 +194,9 @@
|
|||
<object class="GtkGridView" id="library_grid">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="single_click_activate">true</property>
|
||||
<signal name="activate" handler="on_library_grid_clicked"/>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="bytes">
|
||||
<![CDATA[
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="GtkListItem">
|
||||
<property name="activatable">true</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkPicture">
|
||||
<property name="content-fit">contain</property>
|
||||
<property name="can-shrink">false</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="hexpand">true</property>
|
||||
<binding name="tooltip-markup">
|
||||
<lookup name="tooltip" type="GridItem">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
<binding name="paintable">
|
||||
<lookup name="cover" type="GridItem">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
]]>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
|
|
@ -264,17 +212,15 @@
|
|||
<child>
|
||||
<object class="GtkActionBar" id="actionbar">
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="selection_cancel_button">
|
||||
<property name="label" translatable="yes">cancel</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="selection_queue_button">
|
||||
<property name="label" translatable="yes">queue</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_add_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -321,17 +267,15 @@
|
|||
<child>
|
||||
<object class="GtkActionBar" id="actionbar_standalone">
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="standalone_play_button">
|
||||
<property name="label" translatable="yes">play</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="standalone_queue_button">
|
||||
<property name="label" translatable="yes">queue</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_queue_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -340,5 +284,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Select multiple albums</property>
|
||||
<signal name="toggled" handler="on_select_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
|
|
@ -26,7 +25,6 @@
|
|||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Clear the playlist</property>
|
||||
<signal name="clicked" handler="on_clear_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
|
|
@ -37,7 +35,7 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<template class="McgPlaylistPanel" parent="AdwBin">
|
||||
<object id="McgPlaylistPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkStack" id="playlist_stack">
|
||||
<property name="transition-type">slide-left-right</property>
|
||||
|
|
@ -51,45 +49,9 @@
|
|||
<object class="GtkGridView" id="playlist_grid">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="single-click-activate">true</property>
|
||||
<signal name="activate" handler="on_playlist_grid_clicked"/>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="bytes">
|
||||
<![CDATA[
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="GtkListItem">
|
||||
<property name="activatable">true</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkPicture">
|
||||
<property name="content-fit">contain</property>
|
||||
<property name="can-shrink">false</property>
|
||||
<binding name="tooltip-markup">
|
||||
<lookup name="tooltip" type="GridItem">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
<binding name="paintable">
|
||||
<lookup name="cover" type="GridItem">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
]]>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
|
|
@ -103,17 +65,15 @@
|
|||
<child>
|
||||
<object class="GtkActionBar" id="actionbar">
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="selection_cancel_button">
|
||||
<property name="label" translatable="yes">cancel</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="selection_remove_button">
|
||||
<property name="label" translatable="yes">remove</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_remove_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -159,17 +119,15 @@
|
|||
<child>
|
||||
<object class="GtkActionBar" id="actionbar_standalone">
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="standalone_play_button">
|
||||
<property name="label" translatable="yes">play</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton">
|
||||
<object class="GtkButton" id="standalone_remove_button">
|
||||
<property name="label" translatable="yes">remove</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_remove_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -178,5 +136,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<template class="McgServerPanel" parent="AdwBin">
|
||||
<object id="McgServerPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="AdwFlap" id="server_flap">
|
||||
<property name="flap-position">end</property>
|
||||
|
|
@ -322,5 +322,5 @@
|
|||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<interface>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<template class="McgShortcutsDialog" parent="GtkShortcutsWindow">
|
||||
<object id="McgShortcutsDialog" class="GtkShortcutsWindow">
|
||||
<property name="modal">1</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsSection">
|
||||
|
|
@ -103,5 +103,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
<interface>
|
||||
<requires lib="gtk+" version="4.8" />
|
||||
<requires lib="adw" version="1.2" />
|
||||
<template class="McgAppWindow" parent="AdwApplicationWindow">
|
||||
<object id="McgAppWindow" class="AdwApplicationWindow">
|
||||
<property name="title">CoverGrid</property>
|
||||
<property name="content">
|
||||
<object class="AdwToolbarView" id="toolbar_view">
|
||||
<child type="top">
|
||||
|
|
@ -17,13 +18,12 @@
|
|||
</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="headerbar_button_connect">
|
||||
<signal name="state-set" handler="on_headerbar_connection_state_set" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="headerbar_button_playpause">
|
||||
<property name="sensitive">false</property>
|
||||
<property name="tooltip-text" translatable="yes">Switch between play and pause</property>
|
||||
<signal name="toggled" handler="on_headerbar_playpause_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="can-focus">False</property>
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkVolumeButton" id="headerbar_button_volume">
|
||||
<signal name="value-changed" handler="on_headerbar_volume_changed" swapped="no"/>
|
||||
<property name="sensitive">false</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="GtkBox" id="content_box">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="AdwToastOverlay" id="info_toast">
|
||||
|
|
@ -55,7 +55,6 @@
|
|||
<child>
|
||||
<object class="AdwViewStack" id="panel_stack">
|
||||
<property name="vexpand">true</property>
|
||||
<signal name="notify::visible-child" handler="on_stack_switched" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
|
@ -70,9 +69,13 @@
|
|||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkDrawingArea" id="resize_helper">
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/xyz/suruatoel/mcg">
|
||||
<file>gtk.css</file>
|
||||
<file>noise-texture.png</file>
|
||||
<file>ui/window.ui</file>
|
||||
<file>ui/shortcuts-dialog.ui</file>
|
||||
<file>ui/connection-panel.ui</file>
|
||||
<file>ui/album-headerbar.ui</file>
|
||||
<file>ui/server-panel.ui</file>
|
||||
<file>ui/cover-panel.ui</file>
|
||||
<file>ui/playlist-panel.ui</file>
|
||||
<file>ui/library-panel.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<enum id="xyz.suruatoel.mcg.SortOrder">
|
||||
<value nick="artist" value="0" />
|
||||
<value nick="title" value="1" />
|
||||
<value nick="year" value="2" />
|
||||
<value nick="modified" value="3" />
|
||||
</enum>
|
||||
<schema path="/xyz/suruatoel/mcg/" id="xyz.suruatoel.mcg" gettext-domain="mcg">
|
||||
<key type="s" name="host">
|
||||
<default>'localhost'</default>
|
||||
<summary>MPD host</summary>
|
||||
<description>MPD host to connect to</description>
|
||||
</key>
|
||||
<key type="i" name="port">
|
||||
<default>6600</default>
|
||||
<summary>MPD port</summary>
|
||||
<description>MPD port to connect to</description>
|
||||
</key>
|
||||
<key type="b" name="connected">
|
||||
<default>false</default>
|
||||
<summary>Connection state</summary>
|
||||
<description>State of last connection</description>
|
||||
</key>
|
||||
<key type="i" name="width">
|
||||
<default>800</default>
|
||||
<summary>Window width</summary>
|
||||
<description>The window width in pixels.</description>
|
||||
</key>
|
||||
<key type="i" name="height">
|
||||
<default>600</default>
|
||||
<summary>Window height</summary>
|
||||
<description>The window height in pixels.</description>
|
||||
</key>
|
||||
<key type="b" name="is-maximized">
|
||||
<default>false</default>
|
||||
<summary>Window maximized</summary>
|
||||
<description>Whether or not the window is in maximized state.</description>
|
||||
</key>
|
||||
<key type="i" name="panel">
|
||||
<range min="0" max="3"/>
|
||||
<default>1</default>
|
||||
<summary>Last selected panel</summary>
|
||||
<description>The index of the last selected panel.</description>
|
||||
</key>
|
||||
<key type="i" name="item-size">
|
||||
<range min="100" max="1000" />
|
||||
<default>150</default>
|
||||
<summary>Size of library items</summary>
|
||||
<description>The size of items displayed in the library.</description>
|
||||
</key>
|
||||
<key enum="xyz.suruatoel.mcg.SortOrder" name="sort-order">
|
||||
<default>'year'</default>
|
||||
<summary>Sort criterium for library items</summary>
|
||||
<description>The sort criterium of items displayed in the library.</description>
|
||||
</key>
|
||||
<key type="b" name="sort-type">
|
||||
<default>true</default>
|
||||
<summary>Sort type for library items</summary>
|
||||
<description>The sort type of items displayed in the library.</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
|
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7 KiB |
7
go.mod
7
go.mod
|
|
@ -3,3 +3,10 @@ module suruatoel.xyz/mcg
|
|||
go 1.25.0
|
||||
|
||||
toolchain go1.26.0
|
||||
|
||||
require (
|
||||
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20260808200908-d4aecaa0ff32
|
||||
github.com/diamondburned/gotk4/pkg v0.4.1
|
||||
)
|
||||
|
||||
require golang.org/x/sync v0.22.0 // indirect
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -0,0 +1,6 @@
|
|||
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20260808200908-d4aecaa0ff32 h1:1EATAO7ZRrY7B7lt6djCzGnLPlKXPzuOuwPzDSF7jT0=
|
||||
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20260808200908-d4aecaa0ff32/go.mod h1:ZzYiyPe0TqsukfPHi0sK/WwKzm0wIJdSRylLnuvAZNw=
|
||||
github.com/diamondburned/gotk4/pkg v0.4.1 h1:w4BwVUUBkKyNLz5HKHN2Oz3kZCTLHfpfKgB/su1p6uo=
|
||||
github.com/diamondburned/gotk4/pkg v0.4.1/go.mod h1:BAEEDXtj5srQ1S2++5HX2K1s9c6CtuWSa60dAywrkRM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
42
internal/gui/albumheaderbar.go
Normal file
42
internal/gui/albumheaderbar.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
)
|
||||
|
||||
type albumHeaderbar struct {
|
||||
*adw.Bin
|
||||
titleLabel *gtk.Label
|
||||
artistLabel *gtk.Label
|
||||
closeButton *gtk.Button
|
||||
}
|
||||
|
||||
func newAlbumHeaderbar() *albumHeaderbar {
|
||||
bar := albumHeaderbar{}
|
||||
bar.loadWidgets()
|
||||
|
||||
return &bar
|
||||
}
|
||||
|
||||
func (b *albumHeaderbar) loadWidgets() {
|
||||
builder := gtk.NewBuilderFromString(data.AlbumHeaderbarXML)
|
||||
|
||||
b.Bin = builder.GetObject("McgAlbumHeaderbar").Cast().(*adw.Bin)
|
||||
b.titleLabel = builder.GetObject("standalone_title").Cast().(*gtk.Label)
|
||||
b.artistLabel = builder.GetObject("standalone_artist").Cast().(*gtk.Label)
|
||||
b.closeButton = builder.GetObject("close_button").Cast().(*gtk.Button)
|
||||
}
|
||||
|
||||
func (b *albumHeaderbar) SetAlbum(album *common.Album) {
|
||||
b.titleLabel.SetText(album.Title())
|
||||
b.artistLabel.SetText(strings.Join(album.Artists(), ", "))
|
||||
}
|
||||
|
||||
func (b *albumHeaderbar) ConnectClose(callback func()) {
|
||||
b.closeButton.ConnectClicked(callback)
|
||||
}
|
||||
151
internal/gui/application.go
Normal file
151
internal/gui/application.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/gio/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/client"
|
||||
"suruatoel.xyz/mcg/internal/i18n"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
*gtk.Application
|
||||
window *mainWindow
|
||||
aboutDialog *adw.AboutDialog
|
||||
config Configuration
|
||||
configErr error
|
||||
}
|
||||
|
||||
func NewApplication() *Application {
|
||||
app := Application{
|
||||
Application: gtk.NewApplication("xyz.suruatoel.mcg", gio.ApplicationFlagsNone),
|
||||
}
|
||||
app.ConnectStartup(app.onStartup)
|
||||
app.ConnectActivate(app.onActivate)
|
||||
app.ConnectShutdown(app.onShutdown)
|
||||
|
||||
return &app
|
||||
}
|
||||
|
||||
func (a *Application) onStartup() {
|
||||
a.setDefaultSettings()
|
||||
a.loadCSS()
|
||||
a.registerShortcuts()
|
||||
a.registerActions()
|
||||
a.loadConfig()
|
||||
}
|
||||
|
||||
func (a *Application) loadConfig() {
|
||||
a.config = Configuration{
|
||||
Host: "localhost",
|
||||
Port: 6600,
|
||||
Panel: "library",
|
||||
SortField: "year",
|
||||
SortDesc: false,
|
||||
}
|
||||
|
||||
a.configErr = readConfig(&a.config)
|
||||
}
|
||||
|
||||
func (a *Application) setDefaultSettings() {
|
||||
styleManager := adw.StyleManagerGetDefault()
|
||||
styleManager.SetColorScheme(adw.ColorSchemePreferDark)
|
||||
}
|
||||
|
||||
func (a *Application) loadCSS() {
|
||||
styleProvider := gtk.NewCSSProvider()
|
||||
styleProvider.LoadFromString(data.MainCSS)
|
||||
|
||||
gtk.StyleContextAddProviderForDisplay(
|
||||
gdk.DisplayGetDefault(),
|
||||
styleProvider,
|
||||
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
}
|
||||
|
||||
func (a *Application) registerShortcuts() {
|
||||
a.SetAccelsForAction("window.close", []string{"<primary>q"})
|
||||
a.SetAccelsForAction("win.show-help-overlay", []string{"<primary>k"})
|
||||
a.SetAccelsForAction("app.info", []string{"<primary>i"})
|
||||
a.SetAccelsForAction("win.connect", []string{"<primary>c"})
|
||||
a.SetAccelsForAction("win.play", []string{"<primary>p"})
|
||||
a.SetAccelsForAction("win.clear-playlist", []string{"<primary>r"})
|
||||
a.SetAccelsForAction("win.toggle-fullscreen", []string{"F11"})
|
||||
a.SetAccelsForAction("win.search-library", []string{"<primary>f"})
|
||||
a.SetAccelsForAction("win.panel(\"server\")", []string{"<primary>KP_1"})
|
||||
a.SetAccelsForAction("win.panel(\"cover\")", []string{"<primary>KP_2"})
|
||||
a.SetAccelsForAction("win.panel(\"playlist\")", []string{"<primary>KP_3"})
|
||||
a.SetAccelsForAction("win.panel(\"library\")", []string{"<primary>KP_4"})
|
||||
}
|
||||
|
||||
func (a *Application) registerActions() {
|
||||
infoAction := gio.NewSimpleAction("info", nil)
|
||||
infoAction.ConnectActivate(func(_ *glib.Variant) {
|
||||
a.showAboutDialog()
|
||||
})
|
||||
a.AddAction(infoAction)
|
||||
}
|
||||
|
||||
func (a *Application) showAboutDialog() {
|
||||
if a.aboutDialog == nil {
|
||||
a.aboutDialog = a.newAboutDialog()
|
||||
}
|
||||
|
||||
a.aboutDialog.Present(a.window)
|
||||
}
|
||||
|
||||
func (a *Application) newAboutDialog() *adw.AboutDialog {
|
||||
dialog := adw.NewAboutDialog()
|
||||
dialog.SetApplicationName("CoverGrid")
|
||||
dialog.SetApplicationIcon("xyz.suruatoel.mcg")
|
||||
dialog.SetVersion(a.Version())
|
||||
dialog.SetComments(i18n.T("CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks."))
|
||||
dialog.SetWebsite("https://www.suruatoel.xyz/codes/mcg")
|
||||
dialog.SetLicenseType(gtk.LicenseGPL30)
|
||||
dialog.SetIssueURL("https://git.suruatoel.xyz/coderkun/mcg")
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
func (a *Application) onActivate() {
|
||||
if a.window == nil {
|
||||
a.window = newMainWindow(a.Application, a.config)
|
||||
if a.configErr != nil && !os.IsNotExist(a.configErr) {
|
||||
a.window.SetConfigError(a.configErr)
|
||||
}
|
||||
a.window.ConnectCloseRequest(a.onCloseRequest)
|
||||
}
|
||||
|
||||
a.window.Present()
|
||||
if a.config.IsConnected {
|
||||
client.Instance().Connect(a.config.Host, int(a.config.Port), a.config.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) onCloseRequest() bool {
|
||||
a.window.UpdateConfig(&a.config)
|
||||
a.config.IsConnected = client.Instance().IsConnected()
|
||||
|
||||
a.saveConfig()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Application) saveConfig() {
|
||||
err := writeConfig(&a.config)
|
||||
if err != nil {
|
||||
log.Println("failed to save configuration file:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) onShutdown() {
|
||||
if client.Instance().IsConnected() {
|
||||
client.Instance().Disconnect()
|
||||
}
|
||||
}
|
||||
78
internal/gui/config.go
Normal file
78
internal/gui/config.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Host string `json:"host"`
|
||||
Port uint64 `json:"port"`
|
||||
Password string `json:"password"`
|
||||
IsConnected bool `json:"connected"`
|
||||
Panel string `json:"panel"`
|
||||
SortField string `json:"sortField"`
|
||||
SortDesc bool `json:"sortDesc"`
|
||||
ItemSize int `json:"itemSize"`
|
||||
}
|
||||
|
||||
func readConfig(config *Configuration) error {
|
||||
filename, filenameErr := getConfigFile()
|
||||
if filenameErr != nil {
|
||||
return filenameErr
|
||||
}
|
||||
|
||||
file, fileErr := os.Open(filename)
|
||||
if fileErr != nil {
|
||||
return fileErr
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return json.NewDecoder(file).Decode(config)
|
||||
}
|
||||
|
||||
func writeConfig(config *Configuration) error {
|
||||
filename, filenameErr := getConfigFile()
|
||||
if filenameErr != nil {
|
||||
return filenameErr
|
||||
}
|
||||
|
||||
file, fileErr := os.Create(filename)
|
||||
if fileErr != nil {
|
||||
return fileErr
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", "\t")
|
||||
|
||||
return encoder.Encode(&config)
|
||||
}
|
||||
|
||||
func getConfigFile() (string, error) {
|
||||
configDir, configDirErr := getConfigDir()
|
||||
if configDirErr != nil {
|
||||
return "", configDirErr
|
||||
}
|
||||
|
||||
createErr := ensureConfigDirExists(configDir)
|
||||
if createErr != nil {
|
||||
return "", createErr
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, "config.json"), nil
|
||||
}
|
||||
|
||||
func ensureConfigDirExists(folder string) error {
|
||||
return os.MkdirAll(folder, 0o750)
|
||||
}
|
||||
|
||||
func getConfigDir() (string, error) {
|
||||
configDir, configDirErr := os.UserConfigDir()
|
||||
if configDirErr != nil {
|
||||
return "", configDirErr
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, "mcg"), nil
|
||||
}
|
||||
58
internal/gui/connectionpanel.go
Normal file
58
internal/gui/connectionpanel.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
)
|
||||
|
||||
type connectionPanel struct {
|
||||
*adw.Bin
|
||||
|
||||
hostEntry *adw.EntryRow
|
||||
portSpinner *gtk.SpinButton
|
||||
passwordEntry *adw.PasswordEntryRow
|
||||
}
|
||||
|
||||
func newConnectionPanel(config Configuration) *connectionPanel {
|
||||
panel := connectionPanel{}
|
||||
panel.loadWidgets()
|
||||
panel.loadDefaultValues(config)
|
||||
panel.connectSignals()
|
||||
|
||||
return &panel
|
||||
}
|
||||
|
||||
func (p *connectionPanel) loadWidgets() {
|
||||
builder := gtk.NewBuilderFromString(data.ConnectionPanelXML)
|
||||
|
||||
p.Bin = builder.GetObject("McgConnectionPanel").Cast().(*adw.Bin)
|
||||
p.hostEntry = builder.GetObject("host_row").Cast().(*adw.EntryRow)
|
||||
p.portSpinner = builder.GetObject("port_spinner").Cast().(*gtk.SpinButton)
|
||||
p.passwordEntry = builder.GetObject("password_row").Cast().(*adw.PasswordEntryRow)
|
||||
}
|
||||
|
||||
func (p *connectionPanel) connectSignals() {
|
||||
p.hostEntry.ConnectApply(p.callback)
|
||||
p.portSpinner.ConnectValueChanged(p.callback)
|
||||
p.passwordEntry.ConnectApply(p.callback)
|
||||
}
|
||||
|
||||
func (p *connectionPanel) loadDefaultValues(config Configuration) {
|
||||
p.hostEntry.SetText(config.Host)
|
||||
p.portSpinner.SetValue(float64(config.Port))
|
||||
p.passwordEntry.SetText(config.Password)
|
||||
}
|
||||
|
||||
func (p *connectionPanel) UpdateConfig(config *Configuration) {
|
||||
config.Host = p.hostEntry.Text()
|
||||
config.Port = uint64(p.portSpinner.Value())
|
||||
config.Password = p.passwordEntry.Text()
|
||||
}
|
||||
|
||||
func (p *connectionPanel) callback() {
|
||||
}
|
||||
|
||||
func (p *connectionPanel) ConnectionDetails() (string, int, string) {
|
||||
return p.hostEntry.Text(), p.portSpinner.ValueAsInt(), p.passwordEntry.Text()
|
||||
}
|
||||
188
internal/gui/coverloader.go
Normal file
188
internal/gui/coverloader.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/gotk4/pkg/core/gerror"
|
||||
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"suruatoel.xyz/mcg/internal/client"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
)
|
||||
|
||||
type CoverLoader struct {
|
||||
hostDir string
|
||||
size int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewCoverLoader(host string) *CoverLoader {
|
||||
loader := CoverLoader{}
|
||||
loader.hostDir = loader.getHostDir(host)
|
||||
loader.size = loader.readCacheSize()
|
||||
|
||||
return &loader
|
||||
}
|
||||
|
||||
func (l *CoverLoader) SetSize(size int) {
|
||||
if l.size != size {
|
||||
log.Println("cache size has changed from", l.size, "to", size)
|
||||
l.clearCache()
|
||||
l.writeCacheSize(size)
|
||||
l.size = size
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CoverLoader) LoadThumbnail(album *common.Album) *gdkpixbuf.Pixbuf {
|
||||
// Load cover from cache
|
||||
pixbuf := l.getCached(album)
|
||||
if pixbuf != nil {
|
||||
return pixbuf
|
||||
}
|
||||
|
||||
// Load cover from server
|
||||
albumart := client.Instance().LoadAlbumartNow(album)
|
||||
if albumart != nil {
|
||||
pixbuf = l.loadImage(albumart)
|
||||
}
|
||||
if pixbuf != nil {
|
||||
pixbuf = pixbuf.ScaleSimple(l.size, l.size, gdkpixbuf.InterpHyper)
|
||||
if pixbuf != nil {
|
||||
l.setCached(album, pixbuf)
|
||||
}
|
||||
}
|
||||
|
||||
return pixbuf
|
||||
}
|
||||
|
||||
func (l *CoverLoader) getHostDir(host string) string {
|
||||
cacheDir, cacheDirErr := os.UserCacheDir()
|
||||
if cacheDirErr != nil {
|
||||
log.Println("failed to detect user cache directory:", cacheDirErr)
|
||||
return ""
|
||||
}
|
||||
|
||||
hostDir := filepath.Join(cacheDir, "mcg", host)
|
||||
|
||||
return hostDir
|
||||
}
|
||||
|
||||
func (l *CoverLoader) ensureHostDir() {
|
||||
hostDirErr := os.MkdirAll(l.hostDir, 0o750)
|
||||
if hostDirErr != nil {
|
||||
log.Println("failed to create cache folder for host", l.hostDir, ":", hostDirErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CoverLoader) readCacheSize() int {
|
||||
sizeFile := l.getCacheSizeFile()
|
||||
if sizeFile == "" {
|
||||
return 100
|
||||
}
|
||||
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
content, readErr := os.ReadFile(sizeFile)
|
||||
if readErr != nil {
|
||||
log.Println("failed to read cache size file for host", l.hostDir, ":", readErr)
|
||||
return 100
|
||||
}
|
||||
|
||||
size, sizeErr := strconv.Atoi(string(content[:]))
|
||||
if sizeErr != nil {
|
||||
log.Println("invalid content of cache size file for host", l.hostDir, ":", readErr)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
func (l *CoverLoader) writeCacheSize(size int) {
|
||||
l.ensureHostDir()
|
||||
sizeFile := l.getCacheSizeFile()
|
||||
if sizeFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
writeErr := os.WriteFile(sizeFile, []byte(fmt.Sprintf("%d", size)), 0o644)
|
||||
if writeErr != nil {
|
||||
log.Println("failed to write cache size file for host", l.hostDir, ":", writeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CoverLoader) getCacheFile(album *common.Album) string {
|
||||
if l.hostDir == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(l.hostDir, album.ID())
|
||||
}
|
||||
|
||||
func (l *CoverLoader) getCacheSizeFile() string {
|
||||
if l.hostDir == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(l.hostDir, "size")
|
||||
}
|
||||
|
||||
func (l *CoverLoader) clearCache() {
|
||||
if l.hostDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
removeErr := os.RemoveAll(l.hostDir)
|
||||
if removeErr != nil {
|
||||
log.Println("failed to clear cache folder for host", l.hostDir, ":", removeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CoverLoader) getCached(album *common.Album) *gdkpixbuf.Pixbuf {
|
||||
cacheFile := l.getCacheFile(album)
|
||||
if cacheFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pixbuf, pixbufErr := gdkpixbuf.NewPixbufFromFile(cacheFile)
|
||||
if pixbufErr != nil {
|
||||
if !isFileNotFoundError(pixbufErr) {
|
||||
log.Println("failed to read cached thumbnail for host", l.hostDir, ":", pixbufErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return pixbuf
|
||||
}
|
||||
|
||||
func isFileNotFoundError(err error) bool {
|
||||
if gerr, isGerror := err.(*gerror.GError); isGerror {
|
||||
return gerr.Quark() == uint32(glib.FileErrorQuark()) && gerr.ErrorCode() == int(glib.FileErrorNoent)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (l *CoverLoader) setCached(album *common.Album, pixbuf *gdkpixbuf.Pixbuf) {
|
||||
cacheFile := l.getCacheFile(album)
|
||||
if cacheFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
l.ensureHostDir()
|
||||
pixbuf.Savev(cacheFile, "jpeg", []string{}, []string{})
|
||||
}
|
||||
|
||||
func (l *CoverLoader) loadImage(file []byte) *gdkpixbuf.Pixbuf {
|
||||
pixbufLoader, loaderErr := loadPixbuf(file)
|
||||
if loaderErr != nil {
|
||||
log.Println("failed to load image:", loaderErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
return pixbufLoader.Pixbuf()
|
||||
}
|
||||
343
internal/gui/coverpanel.go
Normal file
343
internal/gui/coverpanel.go
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/client"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
"suruatoel.xyz/mcg/internal/i18n"
|
||||
)
|
||||
|
||||
type coverPanel struct {
|
||||
*gtk.Overlay
|
||||
toolbar *gtk.Box
|
||||
fullscreenButton *gtk.Button
|
||||
coverInfoScroll *gtk.ScrolledWindow
|
||||
coverStack *gtk.Stack
|
||||
coverSpinner *gtk.Spinner
|
||||
coverScroll *gtk.ScrolledWindow
|
||||
coverBox *gtk.Viewport
|
||||
coverDefault *gtk.Image
|
||||
coverImage *gtk.Image
|
||||
infoRevealer *gtk.Revealer
|
||||
albumTitleLabel *gtk.Label
|
||||
albumDateLabel *gtk.Label
|
||||
albumArtistLabel *gtk.Label
|
||||
songsScale *gtk.Scale
|
||||
currentAlbum *common.PlaylistAlbum
|
||||
|
||||
currentPixbuf *gdkpixbuf.Pixbuf
|
||||
currentWidth int
|
||||
currentHeight int
|
||||
|
||||
timer *glib.SourceHandle
|
||||
coverBoxController *gtk.GestureClick
|
||||
isFullscreenActive bool
|
||||
}
|
||||
|
||||
func newCoverPanel() *coverPanel {
|
||||
panel := coverPanel{}
|
||||
panel.loadWidgets()
|
||||
panel.connectSignals()
|
||||
|
||||
return &panel
|
||||
}
|
||||
|
||||
func (p *coverPanel) loadWidgets() {
|
||||
builder := gtk.NewBuilderFromString(data.CoverPanelXML)
|
||||
|
||||
p.toolbar = builder.GetObject("toolbar").Cast().(*gtk.Box)
|
||||
p.fullscreenButton = builder.GetObject("fullscreen_button").Cast().(*gtk.Button)
|
||||
p.Overlay = builder.GetObject("McgCoverPanel").Cast().(*gtk.Overlay)
|
||||
p.coverInfoScroll = builder.GetObject("cover_info_scroll").Cast().(*gtk.ScrolledWindow)
|
||||
p.infoRevealer = builder.GetObject("info_revealer").Cast().(*gtk.Revealer)
|
||||
p.albumTitleLabel = builder.GetObject("album_title_label").Cast().(*gtk.Label)
|
||||
p.albumDateLabel = builder.GetObject("album_date_label").Cast().(*gtk.Label)
|
||||
p.albumArtistLabel = builder.GetObject("album_artist_label").Cast().(*gtk.Label)
|
||||
p.songsScale = builder.GetObject("songs_scale").Cast().(*gtk.Scale)
|
||||
p.coverStack = builder.GetObject("cover_stack").Cast().(*gtk.Stack)
|
||||
p.coverSpinner = builder.GetObject("cover_spinner").Cast().(*gtk.Spinner)
|
||||
p.coverScroll = builder.GetObject("cover_scroll").Cast().(*gtk.ScrolledWindow)
|
||||
p.coverBox = builder.GetObject("cover_box").Cast().(*gtk.Viewport)
|
||||
p.coverDefault = builder.GetObject("cover_default").Cast().(*gtk.Image)
|
||||
p.coverImage = builder.GetObject("cover_image").Cast().(*gtk.Image)
|
||||
}
|
||||
|
||||
func (p *coverPanel) connectSignals() {
|
||||
p.coverBoxController = gtk.NewGestureClick()
|
||||
p.coverBox.AddController(p.coverBoxController)
|
||||
|
||||
buttonController := gtk.NewGestureClick()
|
||||
buttonController.ConnectPressed(p.OnSongsScalePressed)
|
||||
buttonController.ConnectUnpairedRelease(p.OnSongsScaleUnpairedReleased)
|
||||
p.songsScale.AddController(buttonController)
|
||||
}
|
||||
|
||||
func (p *coverPanel) ConnectFullscreen(callback func()) {
|
||||
p.fullscreenButton.ConnectClicked(callback)
|
||||
p.coverBoxController.ConnectPressed(func(nPress int, x float64, y float64) {
|
||||
if nPress == 2 && p.currentAlbum != nil {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *coverPanel) OnSongsScalePressed(nPress int, x float64, y float64) {
|
||||
p.clearTimer()
|
||||
}
|
||||
|
||||
func (p *coverPanel) OnSongsScaleUnpairedReleased(x float64, y float64, button uint, sequence *gdk.EventSequence) {
|
||||
value := uint64(p.songsScale.Value())
|
||||
time := p.currentAlbum.Length()
|
||||
songs := p.currentAlbum.Songs()
|
||||
|
||||
pos := 0
|
||||
for index := len(songs) - 1; index >= 0; index-- {
|
||||
time = time - songs[index].Length()
|
||||
pos = int(songs[index].Pos())
|
||||
if time < value {
|
||||
break
|
||||
}
|
||||
}
|
||||
time = max(value-time-1, 0)
|
||||
client.Instance().Seek(pos, time)
|
||||
}
|
||||
|
||||
func (p *coverPanel) Toolbar() gtk.Widgetter {
|
||||
return p.toolbar
|
||||
}
|
||||
|
||||
func (p *coverPanel) SetPlay(pos uint64, time uint64) {
|
||||
p.clearTimer()
|
||||
defer p.startTimer()
|
||||
|
||||
dsongs := p.currentAlbum.Songs()
|
||||
for index := range pos {
|
||||
time += uint64(dsongs[index].Length())
|
||||
}
|
||||
p.songsScale.SetValue(float64(time + 1))
|
||||
}
|
||||
|
||||
func (p *coverPanel) SetPause() {
|
||||
p.clearTimer()
|
||||
}
|
||||
|
||||
func (p *coverPanel) clearTimer() {
|
||||
if p.timer != nil {
|
||||
glib.SourceRemove(*p.timer)
|
||||
p.timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *coverPanel) startTimer() {
|
||||
newTimer := glib.TimeoutAdd(1000, p.playing)
|
||||
p.timer = &newTimer
|
||||
}
|
||||
|
||||
func (p *coverPanel) playing() bool {
|
||||
p.songsScale.SetValue(p.songsScale.Value() + 1)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *coverPanel) SetAlbum(state string, album *common.PlaylistAlbum) {
|
||||
if album != nil {
|
||||
// Set labels
|
||||
p.albumTitleLabel.SetLabel(album.Title())
|
||||
p.albumDateLabel.SetLabel(strings.Join(album.Dates(), ", "))
|
||||
p.albumArtistLabel.SetLabel(strings.Join(album.Artists(), ","))
|
||||
p.infoRevealer.SetRevealChild(true)
|
||||
|
||||
p.setSongs(album)
|
||||
} else {
|
||||
p.infoRevealer.SetRevealChild(false)
|
||||
}
|
||||
|
||||
// Load cover
|
||||
if album != nil {
|
||||
p.coverSpinner.Start()
|
||||
p.coverStack.SetVisibleChild(p.coverSpinner)
|
||||
|
||||
client.Instance().LoadAlbumart(album.Album)
|
||||
}
|
||||
|
||||
// Set current album
|
||||
p.currentAlbum = album
|
||||
p.fullscreenButton.SetSensitive(album != nil)
|
||||
p.enableSonglist()
|
||||
}
|
||||
|
||||
func (p *coverPanel) Fullscreen(active bool) {
|
||||
p.isFullscreenActive = active
|
||||
p.enableSonglist()
|
||||
glib.IdleAdd(p.resizeImage)
|
||||
}
|
||||
|
||||
func (p *coverPanel) enableSonglist() {
|
||||
enable := p.currentAlbum != nil && !p.isFullscreenActive
|
||||
|
||||
p.infoRevealer.SetRevealChild(enable)
|
||||
}
|
||||
|
||||
func (p *coverPanel) SetAlbumart(album *common.Album, albumart []byte) {
|
||||
if p.currentAlbum == nil || album != p.currentAlbum.Album {
|
||||
return
|
||||
}
|
||||
|
||||
defer glib.IdleAdd(func() {
|
||||
p.coverStack.SetVisibleChild(p.coverScroll)
|
||||
p.coverSpinner.Stop()
|
||||
})
|
||||
|
||||
p.currentPixbuf = nil
|
||||
if albumart == nil || len(albumart) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
loader, loaderErr := loadPixbuf(albumart)
|
||||
if loaderErr != nil {
|
||||
log.Println("failed to load albumart for ", album.Title(), ":", loaderErr)
|
||||
return
|
||||
}
|
||||
|
||||
p.currentPixbuf = loader.Pixbuf()
|
||||
glib.IdleAdd(p.showImage)
|
||||
}
|
||||
|
||||
func (p *coverPanel) showImage() {
|
||||
if p.currentPixbuf != nil {
|
||||
p.resizeImage()
|
||||
p.coverStack.SetVisibleChild(p.coverScroll)
|
||||
} else {
|
||||
p.coverStack.SetVisibleChild(p.coverDefault)
|
||||
}
|
||||
}
|
||||
|
||||
func loadPixbuf(data []byte) (*gdkpixbuf.PixbufLoader, error) {
|
||||
loader := gdkpixbuf.NewPixbufLoader()
|
||||
defer loader.Close()
|
||||
|
||||
return loader, loader.Write(data)
|
||||
}
|
||||
|
||||
func (p *coverPanel) setSongs(album *common.PlaylistAlbum) {
|
||||
p.songsScale.ClearMarks()
|
||||
p.songsScale.SetRange(0, float64(album.Length()))
|
||||
|
||||
length := uint64(0)
|
||||
for _, song := range album.Songs() {
|
||||
curLength := length
|
||||
if length > 0 && length < album.Length() {
|
||||
curLength += 1
|
||||
}
|
||||
songTitle := createSongTitle(song)
|
||||
p.songsScale.AddMark(
|
||||
float64(curLength),
|
||||
gtk.PosRight,
|
||||
glib.MarkupEscapeText(songTitle),
|
||||
)
|
||||
length += song.Length()
|
||||
}
|
||||
|
||||
p.songsScale.AddMark(float64(length), gtk.PosRight, createAlbumLengthTitle(length))
|
||||
p.alignSongsScaleMarks()
|
||||
}
|
||||
|
||||
func (p *coverPanel) alignSongsScaleMarks() {
|
||||
p.alignScaleMarks(&p.songsScale.Widget)
|
||||
}
|
||||
|
||||
func (p *coverPanel) alignScaleMarks(widgetter gtk.Widgetter) {
|
||||
widget := widgetter.(*gtk.Widget)
|
||||
child := widget.FirstChild()
|
||||
for child != nil {
|
||||
if label, isLabel := child.(*gtk.Label); isLabel {
|
||||
label.SetHAlign(gtk.AlignStart)
|
||||
child = label.NextSibling()
|
||||
} else {
|
||||
p.alignScaleMarks(child)
|
||||
child = child.(*gtk.Widget).NextSibling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createSongTitle(song *common.PlaylistSong) string {
|
||||
songArtists := song.SongArtists()
|
||||
if len(songArtists) > 0 {
|
||||
return fmt.Sprintf(
|
||||
i18n.T("%s feat. %s"),
|
||||
song.Title(),
|
||||
strings.Join(songArtists, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
return song.Title()
|
||||
}
|
||||
|
||||
func createAlbumLengthTitle(length uint64) string {
|
||||
minutes := length / 60
|
||||
seconds := length % 60
|
||||
|
||||
return fmt.Sprintf(i18n.T("%d:%d minutes"), minutes, seconds)
|
||||
}
|
||||
|
||||
func (p *coverPanel) SetWidth(width int) {
|
||||
glib.IdleAdd(p.resizeImage)
|
||||
glib.IdleAdd(func() {
|
||||
p.coverInfoScroll.SetMaxContentWidth(width / 2)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *coverPanel) resizeImage() {
|
||||
newWidth, newHeight := p.getSize()
|
||||
if p.hasSizeChanged(newWidth, newHeight) {
|
||||
return
|
||||
}
|
||||
p.setSize(newWidth, newHeight)
|
||||
|
||||
if p.currentPixbuf == nil {
|
||||
p.coverImage.SetPixelSize(min(newWidth, newHeight))
|
||||
return
|
||||
}
|
||||
|
||||
width, height := p.calculateCoverSize()
|
||||
if width <= 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
scaledPixbuf := p.currentPixbuf.ScaleSimple(width, height, gdkpixbuf.InterpHyper)
|
||||
p.coverImage.SetFromPaintable(gdk.NewTextureForPixbuf(scaledPixbuf))
|
||||
p.coverImage.SetPixelSize(min(width, height))
|
||||
}
|
||||
|
||||
func (p *coverPanel) getSize() (int, int) {
|
||||
return p.coverStack.Size(gtk.OrientationHorizontal), p.coverStack.Size(gtk.OrientationVertical)
|
||||
}
|
||||
|
||||
func (p *coverPanel) hasSizeChanged(newWidth, newHeight int) bool {
|
||||
return newWidth == p.currentWidth && newHeight == p.currentHeight
|
||||
}
|
||||
|
||||
func (p *coverPanel) setSize(newWidth, newHeight int) {
|
||||
p.currentWidth = newWidth
|
||||
p.currentHeight = newHeight
|
||||
}
|
||||
|
||||
func (p *coverPanel) calculateCoverSize() (width, height int) {
|
||||
ratioWidth := float64(p.currentWidth) / float64(p.currentPixbuf.Width())
|
||||
ratioHeight := float64(p.currentHeight) / float64(p.currentPixbuf.Height())
|
||||
ratio := min(min(ratioWidth, ratioHeight), 1)
|
||||
|
||||
width = int(math.Floor(float64(p.currentPixbuf.Width()) * ratio))
|
||||
height = int(math.Floor(float64(p.currentPixbuf.Height()) * ratio))
|
||||
|
||||
return
|
||||
}
|
||||
563
internal/gui/librarypanel.go
Normal file
563
internal/gui/librarypanel.go
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/client"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
)
|
||||
|
||||
type libraryPanel struct {
|
||||
*adw.Bin
|
||||
toolbar *gtk.Box
|
||||
selectButton *gtk.ToggleButton
|
||||
actionbarRevealer *gtk.Revealer
|
||||
toolbarPopover *gtk.Popover
|
||||
searchBar *gtk.SearchBar
|
||||
searchEntry *gtk.SearchEntry
|
||||
sortButtons map[string]*gtk.CheckButton
|
||||
sortDescButton *gtk.CheckButton
|
||||
gridScale *gtk.Scale
|
||||
stack *gtk.Stack
|
||||
progressBox *gtk.Box
|
||||
progressBar *gtk.ProgressBar
|
||||
scroll *gtk.ScrolledWindow
|
||||
libraryGrid *gtk.GridView
|
||||
gridFactory *pictureItemFactory[string]
|
||||
libraryModel *gtk.StringList
|
||||
selectionMulti *gtk.MultiSelection
|
||||
selectionSingle *gtk.SingleSelection
|
||||
|
||||
headerbarStandalone *albumHeaderbar
|
||||
libraryStack *gtk.Stack
|
||||
panelNormal *gtk.Box
|
||||
panelStandalone *gtk.Box
|
||||
standaloneStack *gtk.Stack
|
||||
standaloneSpinner *gtk.Spinner
|
||||
standaloneScroll *gtk.ScrolledWindow
|
||||
standaloneImage *gtk.Image
|
||||
standalonePlayButton *gtk.Button
|
||||
standaloneQueueButton *gtk.Button
|
||||
selectionQueueButton *gtk.Button
|
||||
selectionCancelButton *gtk.Button
|
||||
|
||||
coverLoader *CoverLoader
|
||||
albums map[string]*common.Album
|
||||
pixbufs map[string]*gdkpixbuf.Pixbuf
|
||||
sortField string
|
||||
sortDesc bool
|
||||
itemSize int
|
||||
itemSizeCallback ItemSizeCallback
|
||||
|
||||
libraryLock sync.Mutex
|
||||
libraryWait sync.WaitGroup
|
||||
|
||||
standaloneAlbum *common.Album
|
||||
standalonePixbuf *gdkpixbuf.Pixbuf
|
||||
standaloneCallback func(bool)
|
||||
}
|
||||
|
||||
type ItemSizeCallback func(itemSize int)
|
||||
|
||||
func newLibraryPanel(config Configuration) *libraryPanel {
|
||||
panel := libraryPanel{
|
||||
albums: make(map[string]*common.Album),
|
||||
pixbufs: make(map[string]*gdkpixbuf.Pixbuf),
|
||||
sortField: "year",
|
||||
sortDesc: false,
|
||||
libraryLock: sync.Mutex{},
|
||||
libraryWait: sync.WaitGroup{},
|
||||
}
|
||||
panel.loadWidgets(config)
|
||||
panel.loadDefaultValues(config)
|
||||
panel.connectSignals()
|
||||
|
||||
return &panel
|
||||
}
|
||||
|
||||
func (p *libraryPanel) loadWidgets(config Configuration) {
|
||||
builder := gtk.NewBuilderFromString(data.LibraryPanelXML)
|
||||
|
||||
p.toolbar = builder.GetObject("toolbar").Cast().(*gtk.Box)
|
||||
p.selectButton = builder.GetObject("select_button").Cast().(*gtk.ToggleButton)
|
||||
p.actionbarRevealer = builder.GetObject("actionbar_revealer").Cast().(*gtk.Revealer)
|
||||
p.toolbarPopover = builder.GetObject("toolbar_popover").Cast().(*gtk.Popover)
|
||||
|
||||
p.searchBar = builder.GetObject("filter_bar").Cast().(*gtk.SearchBar)
|
||||
p.searchEntry = builder.GetObject("filter_entry").Cast().(*gtk.SearchEntry)
|
||||
p.sortButtons = make(map[string]*gtk.CheckButton)
|
||||
p.sortButtons["artist"] = builder.GetObject("sort_artist").Cast().(*gtk.CheckButton)
|
||||
p.sortButtons["title"] = builder.GetObject("sort_title").Cast().(*gtk.CheckButton)
|
||||
p.sortButtons["year"] = builder.GetObject("sort_year").Cast().(*gtk.CheckButton)
|
||||
p.sortButtons["modified"] = builder.GetObject("sort_modified").Cast().(*gtk.CheckButton)
|
||||
p.sortDescButton = builder.GetObject("toolbar_sort_order_button").Cast().(*gtk.CheckButton)
|
||||
p.gridScale = builder.GetObject("grid_scale").Cast().(*gtk.Scale)
|
||||
|
||||
p.Bin = builder.GetObject("McgLibraryPanel").Cast().(*adw.Bin)
|
||||
p.stack = builder.GetObject("stack").Cast().(*gtk.Stack)
|
||||
p.progressBox = builder.GetObject("progress_box").Cast().(*gtk.Box)
|
||||
p.progressBar = builder.GetObject("progress_bar").Cast().(*gtk.ProgressBar)
|
||||
p.scroll = builder.GetObject("scroll").Cast().(*gtk.ScrolledWindow)
|
||||
p.libraryGrid = builder.GetObject("library_grid").Cast().(*gtk.GridView)
|
||||
p.libraryModel = gtk.NewStringList(nil)
|
||||
p.selectionMulti = gtk.NewMultiSelection(p.libraryModel)
|
||||
p.selectionSingle = gtk.NewSingleSelection(p.libraryModel)
|
||||
p.libraryGrid.SetModel(p.selectionSingle)
|
||||
|
||||
p.gridFactory = newPictureItemFactory[string](p.itemToModelID, p.modelIDToValue)
|
||||
p.libraryGrid.SetFactory(p.gridFactory.ListItemFactory())
|
||||
|
||||
// Headerbar
|
||||
p.headerbarStandalone = newAlbumHeaderbar()
|
||||
p.libraryStack = builder.GetObject("library_stack").Cast().(*gtk.Stack)
|
||||
p.panelNormal = builder.GetObject("panel_normal").Cast().(*gtk.Box)
|
||||
p.panelStandalone = builder.GetObject("panel_standalone").Cast().(*gtk.Box)
|
||||
|
||||
p.standaloneStack = builder.GetObject("standalone_stack").Cast().(*gtk.Stack)
|
||||
p.standaloneSpinner = builder.GetObject("standalone_spinner").Cast().(*gtk.Spinner)
|
||||
p.standaloneScroll = builder.GetObject("standalone_scroll").Cast().(*gtk.ScrolledWindow)
|
||||
p.standaloneImage = builder.GetObject("standalone_image").Cast().(*gtk.Image)
|
||||
p.standalonePlayButton = builder.GetObject("standalone_play_button").Cast().(*gtk.Button)
|
||||
p.standaloneQueueButton = builder.GetObject("standalone_queue_button").Cast().(*gtk.Button)
|
||||
|
||||
// Selection
|
||||
p.selectionQueueButton = builder.GetObject("selection_queue_button").Cast().(*gtk.Button)
|
||||
p.selectionCancelButton = builder.GetObject("selection_cancel_button").Cast().(*gtk.Button)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) itemToModelID(item *glib.Object) string {
|
||||
return item.Cast().(*gtk.StringObject).String()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) modelIDToValue(itemID string) (tooltip string, pixbuf *gdkpixbuf.Pixbuf) {
|
||||
if album, ok := p.albums[itemID]; ok {
|
||||
tooltip = album.Tooltip()
|
||||
}
|
||||
pixbuf = p.pixbufs[itemID]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *libraryPanel) loadDefaultValues(config Configuration) {
|
||||
p.itemSize = config.ItemSize
|
||||
p.gridScale.SetValue(float64(config.ItemSize))
|
||||
|
||||
_, found := p.sortButtons[config.SortField]
|
||||
if found {
|
||||
p.sortField = config.SortField
|
||||
}
|
||||
p.sortDesc = config.SortDesc
|
||||
|
||||
p.sortButtons[p.sortField].SetActive(true)
|
||||
p.sortDescButton.SetActive(p.sortDesc)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) connectSignals() {
|
||||
p.selectButton.ConnectToggled(p.onSelectToggled)
|
||||
p.searchEntry.ConnectSearchChanged(p.onFilterEntryChanged)
|
||||
for field, button := range p.sortButtons {
|
||||
button.ConnectToggled(func() {
|
||||
p.onSortToggled(field, button)
|
||||
})
|
||||
}
|
||||
p.sortDescButton.ConnectToggled(p.onSortDescToggled)
|
||||
|
||||
p.libraryGrid.ConnectActivate(p.onGridClicked)
|
||||
|
||||
p.headerbarStandalone.ConnectClose(p.onHeaderbarStandaloneClose)
|
||||
p.standalonePlayButton.ConnectClicked(p.onStandalonePlayClicked)
|
||||
p.standaloneQueueButton.ConnectClicked(p.onStandaloneQueueClicked)
|
||||
|
||||
p.selectionQueueButton.ConnectClicked(p.onSelectionQueue)
|
||||
p.selectionCancelButton.ConnectClicked(p.onSelectionCancel)
|
||||
|
||||
buttonController := gtk.NewGestureClick()
|
||||
buttonController.ConnectUnpairedRelease(p.onGridScaleReleased)
|
||||
p.gridScale.AddController(buttonController)
|
||||
p.gridScale.ConnectValueChanged(p.onGridScaleChanged)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) ConnectItemSizeChanged(callback ItemSizeCallback) {
|
||||
p.itemSizeCallback = callback
|
||||
}
|
||||
|
||||
func (p *libraryPanel) ConnectStandalone(callback func(bool)) {
|
||||
p.standaloneCallback = callback
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onSelectToggled() {
|
||||
if p.selectButton.Active() {
|
||||
p.actionbarRevealer.SetRevealChild(true)
|
||||
p.libraryGrid.SetModel(p.selectionMulti)
|
||||
p.libraryGrid.SetSingleClickActivate(false)
|
||||
p.libraryGrid.StyleContext().AddClass("selection")
|
||||
} else {
|
||||
p.actionbarRevealer.SetRevealChild(false)
|
||||
p.libraryGrid.SetModel(p.selectionSingle)
|
||||
p.libraryGrid.SetSingleClickActivate(true)
|
||||
p.libraryGrid.StyleContext().RemoveClass("selection")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onFilterEntryChanged() {
|
||||
p.updateGridModel()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onSortToggled(field string, button *gtk.CheckButton) {
|
||||
if !button.Active() {
|
||||
return
|
||||
}
|
||||
|
||||
p.sortField = field
|
||||
p.updateGridModel()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onSortDescToggled() {
|
||||
p.sortDesc = p.sortDescButton.Active()
|
||||
p.updateGridModel()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onGridScaleChanged() {
|
||||
size := math.Floor(p.gridScale.Value())
|
||||
gridRange := p.gridScale.Adjustment()
|
||||
if size < gridRange.Lower() || size > gridRange.Upper() {
|
||||
return
|
||||
}
|
||||
|
||||
go p.setGridSize(int(size))
|
||||
}
|
||||
|
||||
func (p *libraryPanel) setGridSize(size int) {
|
||||
if size == p.itemSize {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range p.libraryGrid.Model().NItems() {
|
||||
itemObj := p.libraryGrid.Model().Item(i)
|
||||
if itemObj != nil {
|
||||
itemStr := itemObj.Cast().(*gtk.StringObject)
|
||||
albumID := itemStr.String()
|
||||
picture, pictureFound := p.gridFactory.Picture(albumID)
|
||||
if pictureFound {
|
||||
pixbuf, pixbufFound := p.pixbufs[albumID]
|
||||
if pixbufFound {
|
||||
if pixbuf != nil {
|
||||
pixbuf = pixbuf.ScaleSimple(size, size, gdkpixbuf.InterpNearest)
|
||||
} else {
|
||||
}
|
||||
glib.IdleAdd(func() {
|
||||
picture.SetPaintable(gdk.NewTextureForPixbuf(pixbuf))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onGridScaleReleased(x float64, y float64, button uint, sequence *gdk.EventSequence) {
|
||||
size := math.Floor(p.gridScale.Value())
|
||||
gridRange := p.gridScale.Adjustment()
|
||||
if size < gridRange.Lower() || size > gridRange.Upper() {
|
||||
return
|
||||
}
|
||||
|
||||
p.itemSize = int(size)
|
||||
p.redraw()
|
||||
glib.IdleAdd(p.toolbarPopover.Popdown)
|
||||
if p.itemSizeCallback != nil {
|
||||
p.itemSizeCallback(p.itemSize)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) redraw() {
|
||||
if len(p.albums) > 0 {
|
||||
p.SetAlbums(p.albums)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onGridClicked(position uint) {
|
||||
// Get selected album
|
||||
itemObj := p.libraryModel.Item(position)
|
||||
if itemObj == nil {
|
||||
return
|
||||
}
|
||||
strObj := itemObj.Cast().(*gtk.StringObject)
|
||||
id := strObj.String()
|
||||
album, found := p.albums[id]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
// Show standalone album
|
||||
if p.libraryGrid.Model().Native() == p.selectionSingle.Native() {
|
||||
p.standaloneAlbum = album
|
||||
p.headerbarStandalone.SetAlbum(album)
|
||||
p.standaloneStack.SetVisibleChild(p.standaloneSpinner)
|
||||
p.standaloneSpinner.Start()
|
||||
p.openStandalone()
|
||||
client.Instance().LoadAlbumart(album)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onHeaderbarStandaloneClose() {
|
||||
p.closeStandalone()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) openStandalone() {
|
||||
p.libraryStack.SetVisibleChild(p.panelStandalone)
|
||||
if p.standaloneCallback != nil {
|
||||
p.standaloneCallback(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) closeStandalone() {
|
||||
p.libraryStack.SetVisibleChild(p.panelNormal)
|
||||
if p.standaloneCallback != nil {
|
||||
p.standaloneCallback(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) setCoverLoader(loader *CoverLoader) {
|
||||
p.coverLoader = loader
|
||||
}
|
||||
|
||||
func (p *libraryPanel) SetAlbumart(album *common.Album, albumart []byte) {
|
||||
if album != p.standaloneAlbum {
|
||||
return
|
||||
}
|
||||
|
||||
p.standalonePixbuf = nil
|
||||
if albumart == nil || len(albumart) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
loader, loaderErr := loadPixbuf(albumart)
|
||||
if loaderErr != nil {
|
||||
log.Println("failed to load albumart for ", album.Title(), ":", loaderErr)
|
||||
return
|
||||
}
|
||||
|
||||
p.standalonePixbuf = loader.Pixbuf()
|
||||
glib.IdleAdd(p.showStandaloneImage)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) showStandaloneImage() {
|
||||
p.resizeStandaloneImage()
|
||||
p.standaloneStack.SetVisibleChild(p.standaloneScroll)
|
||||
p.standaloneSpinner.Stop()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) resizeStandaloneImage() {
|
||||
newWidth, newHeight := p.getStandaloneSize()
|
||||
if p.standalonePixbuf == nil {
|
||||
p.standaloneImage.SetPixelSize(min(newWidth, newHeight))
|
||||
return
|
||||
}
|
||||
|
||||
width, height := p.calculateCoverSize(newWidth, newHeight)
|
||||
if width <= 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
scaledPixbuf := p.standalonePixbuf.ScaleSimple(width, height, gdkpixbuf.InterpHyper)
|
||||
p.standaloneImage.SetFromPaintable(gdk.NewTextureForPixbuf(scaledPixbuf))
|
||||
p.standaloneImage.SetPixelSize(min(width, height))
|
||||
}
|
||||
|
||||
func (p *libraryPanel) getStandaloneSize() (int, int) {
|
||||
return p.standaloneStack.Size(gtk.OrientationHorizontal), p.standaloneStack.Size(gtk.OrientationVertical)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) calculateCoverSize(currentWidth, currentHeight int) (width, height int) {
|
||||
ratioWidth := float64(currentWidth) / float64(p.standalonePixbuf.Width())
|
||||
ratioHeight := float64(currentHeight) / float64(p.standalonePixbuf.Height())
|
||||
ratio := min(min(ratioWidth, ratioHeight), 1)
|
||||
|
||||
width = int(math.Floor(float64(p.standalonePixbuf.Width()) * ratio))
|
||||
height = int(math.Floor(float64(p.standalonePixbuf.Height()) * ratio))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onStandalonePlayClicked() {
|
||||
client.Instance().PlayAlbum(p.standaloneAlbum)
|
||||
p.closeStandalone()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onStandaloneQueueClicked() {
|
||||
client.Instance().QueueAlbum(p.standaloneAlbum)
|
||||
p.closeStandalone()
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onSelectionQueue() {
|
||||
albums := p.getSelectedAlbums()
|
||||
if len(albums) > 0 {
|
||||
client.Instance().QueueAlbums(albums)
|
||||
}
|
||||
|
||||
p.selectButton.SetActive(false)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) onSelectionCancel() {
|
||||
p.selectButton.SetActive(false)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) getSelectedAlbums() []*common.Album {
|
||||
albums := []*common.Album{}
|
||||
for i := range p.selectionMulti.NItems() {
|
||||
if p.selectionMulti.IsSelected(i) {
|
||||
itemObj := p.selectionMulti.Item(i)
|
||||
if itemObj != nil {
|
||||
itemStr := itemObj.Cast().(*gtk.StringObject)
|
||||
albumID := itemStr.String()
|
||||
album, found := p.albums[albumID]
|
||||
if found {
|
||||
albums = append(albums, album)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return albums
|
||||
}
|
||||
|
||||
func (p *libraryPanel) UpdateConfig(config *Configuration) {
|
||||
config.SortField = p.sortField
|
||||
config.SortDesc = p.sortDesc
|
||||
config.ItemSize = p.itemSize
|
||||
}
|
||||
|
||||
func (p *libraryPanel) updateGridModel() {
|
||||
albums := p.filterAlbums()
|
||||
p.sortAlbums(albums)
|
||||
p.updateGrid(albums)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) filterAlbums() []*common.Album {
|
||||
var matchingAlbums []*common.Album
|
||||
query := strings.TrimSpace(strings.ToLower(p.searchEntry.Text()))
|
||||
for _, album := range p.albums {
|
||||
if album.IsMatch(query) {
|
||||
matchingAlbums = append(matchingAlbums, album)
|
||||
}
|
||||
}
|
||||
|
||||
return matchingAlbums
|
||||
}
|
||||
|
||||
func (p *libraryPanel) sortAlbums(filtered []*common.Album) {
|
||||
common.SortAlbums(filtered, p.sortField)
|
||||
|
||||
if p.sortDesc {
|
||||
slices.Reverse(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) updateGrid(albums []*common.Album) {
|
||||
glib.IdleAdd(func() {
|
||||
n := p.libraryModel.NItems()
|
||||
if n > 0 {
|
||||
p.libraryModel.Splice(0, n, nil)
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(albums))
|
||||
for _, item := range albums {
|
||||
ids = append(ids, item.ID())
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
p.libraryModel.Splice(0, 0, ids)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *libraryPanel) Toolbar() gtk.Widgetter {
|
||||
return p.toolbar
|
||||
}
|
||||
|
||||
func (p *libraryPanel) Wait() {
|
||||
p.libraryWait.Wait()
|
||||
}
|
||||
|
||||
type LibraryFactory gtk.ListItemFactory
|
||||
|
||||
func (p *libraryPanel) SetAlbums(albums map[string]*common.Album) {
|
||||
p.libraryWait.Add(1)
|
||||
go p.setAlbums(albums)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) setAlbums(albums map[string]*common.Album) {
|
||||
p.libraryLock.Lock()
|
||||
defer p.libraryLock.Unlock()
|
||||
defer p.libraryWait.Done()
|
||||
|
||||
p.albums = albums
|
||||
glib.IdleAdd(func() {
|
||||
p.stack.SetVisibleChild(p.progressBox)
|
||||
p.progressBar.SetFraction(0.0)
|
||||
})
|
||||
|
||||
// Create loader loader
|
||||
i := 0
|
||||
n := len(albums)
|
||||
p.pixbufs = make(map[string]*gdkpixbuf.Pixbuf)
|
||||
|
||||
for _, album := range albums {
|
||||
if p.coverLoader != nil {
|
||||
p.pixbufs[album.ID()] = p.coverLoader.LoadThumbnail(album)
|
||||
}
|
||||
|
||||
i++
|
||||
glib.IdleAdd(func() {
|
||||
p.progressBar.SetFraction(float64(i) / float64(n))
|
||||
// FIXME: i18n
|
||||
p.progressBar.SetText("Loading images")
|
||||
})
|
||||
}
|
||||
|
||||
p.updateGridModel()
|
||||
glib.IdleAdd(func() {
|
||||
p.stack.SetVisibleChild(p.scroll)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *libraryPanel) SetWidth(width int) {
|
||||
p.resizeStandaloneImage()
|
||||
p.setMarks(width)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) setMarks(width int) {
|
||||
glib.IdleAdd(func() {
|
||||
p.gridScale.ClearMarks()
|
||||
})
|
||||
|
||||
lower := p.gridScale.Adjustment().Lower()
|
||||
upper := p.gridScale.Adjustment().Upper()
|
||||
countMin := int(math.Max(float64(width)/upper, 1))
|
||||
countMax := int(math.Max(float64(width)/lower, 1))
|
||||
for i := countMin; i <= countMax; i++ {
|
||||
pixel := int(float64(width) / float64(i))
|
||||
pixel = pixel - (2 * pixel / 100)
|
||||
glib.IdleAdd(func() {
|
||||
p.gridScale.AddMark(float64(pixel), gtk.PosBottom, "")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *libraryPanel) ShowSearch() {
|
||||
p.searchBar.SetSearchMode(true)
|
||||
}
|
||||
|
||||
func (p *libraryPanel) HeaderbarStandalone() *albumHeaderbar {
|
||||
return p.headerbarStandalone
|
||||
}
|
||||
78
internal/gui/pictureitemfactory.go
Normal file
78
internal/gui/pictureitemfactory.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/gotk4/pkg/core/glib"
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
)
|
||||
|
||||
type pictureItemFactory[T comparable] struct {
|
||||
model modelAccessor[T]
|
||||
value valueAccessor[T]
|
||||
listItemFactory *gtk.SignalListItemFactory
|
||||
pictures map[T]*gtk.Picture
|
||||
}
|
||||
|
||||
type (
|
||||
modelAccessor[T any] func(item *glib.Object) T
|
||||
valueAccessor[T any] func(T) (tooltip string, pixbuf *gdkpixbuf.Pixbuf)
|
||||
)
|
||||
|
||||
func newPictureItemFactory[T comparable](model modelAccessor[T], value valueAccessor[T]) *pictureItemFactory[T] {
|
||||
factory := pictureItemFactory[T]{
|
||||
model: model,
|
||||
value: value,
|
||||
listItemFactory: gtk.NewSignalListItemFactory(),
|
||||
pictures: make(map[T]*gtk.Picture),
|
||||
}
|
||||
factory.listItemFactory.Connect("setup", factory.setup)
|
||||
factory.listItemFactory.Connect("bind", factory.bind)
|
||||
factory.listItemFactory.Connect("unbind", factory.unbind)
|
||||
|
||||
return &factory
|
||||
}
|
||||
|
||||
func (f *pictureItemFactory[T]) setup(listitem *gtk.ListItem) {
|
||||
picture := gtk.NewPicture()
|
||||
picture.SetContentFit(gtk.ContentFitContain)
|
||||
picture.SetCanShrink(false)
|
||||
listitem.SetChild(picture)
|
||||
}
|
||||
|
||||
func (f *pictureItemFactory[T]) bind(listitem *gtk.ListItem) {
|
||||
item := listitem.Item()
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
itemID := f.model(item)
|
||||
|
||||
tooltip, pixbuf := f.value(itemID)
|
||||
picture := listitem.Child().(*gtk.Picture)
|
||||
picture.SetTooltipText(tooltip)
|
||||
if pixbuf != nil {
|
||||
picture.SetPaintable(gdk.NewTextureForPixbuf(pixbuf))
|
||||
}
|
||||
|
||||
f.pictures[itemID] = picture
|
||||
}
|
||||
|
||||
func (f *pictureItemFactory[T]) unbind(listitem *gtk.ListItem) {
|
||||
itemObj := listitem.Item()
|
||||
if itemObj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
itemID := f.model(itemObj)
|
||||
delete(f.pictures, itemID)
|
||||
}
|
||||
|
||||
func (f *pictureItemFactory[T]) ListItemFactory() *gtk.ListItemFactory {
|
||||
return &f.listItemFactory.ListItemFactory
|
||||
}
|
||||
|
||||
func (f *pictureItemFactory[T]) Picture(itemID T) (*gtk.Picture, bool) {
|
||||
picture, found := f.pictures[itemID]
|
||||
|
||||
return picture, found
|
||||
}
|
||||
358
internal/gui/playlistpanel.go
Normal file
358
internal/gui/playlistpanel.go
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/client"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
)
|
||||
|
||||
type playlistPanel struct {
|
||||
*adw.Bin
|
||||
toolbar *gtk.Box
|
||||
selectButton *gtk.ToggleButton
|
||||
actionbarRevealer *gtk.Revealer
|
||||
clearButton *gtk.Button
|
||||
playlistGrid *gtk.GridView
|
||||
gridFactory *pictureItemFactory[string]
|
||||
playlistModel *gtk.StringList
|
||||
selectionMulti *gtk.MultiSelection
|
||||
selectionSingle *gtk.SingleSelection
|
||||
|
||||
headerbarStandalone *albumHeaderbar
|
||||
playlistStack *gtk.Stack
|
||||
panelNormal *gtk.Box
|
||||
panelStandalone *gtk.Box
|
||||
standaloneStack *gtk.Stack
|
||||
standaloneSpinner *gtk.Spinner
|
||||
standaloneScroll *gtk.ScrolledWindow
|
||||
standaloneImage *gtk.Image
|
||||
standalonePlayButton *gtk.Button
|
||||
standaloneRemoveButton *gtk.Button
|
||||
|
||||
selectionRemoveButton *gtk.Button
|
||||
selectionCancelButton *gtk.Button
|
||||
|
||||
coverLoader *CoverLoader
|
||||
playlist []*common.PlaylistAlbum
|
||||
albums map[string]*common.PlaylistAlbum
|
||||
pixbufs map[string]*gdkpixbuf.Pixbuf
|
||||
|
||||
standaloneAlbum *common.PlaylistAlbum
|
||||
standalonePixbuf *gdkpixbuf.Pixbuf
|
||||
standaloneCallback func(bool)
|
||||
}
|
||||
|
||||
func newPlaylistPanel() *playlistPanel {
|
||||
panel := playlistPanel{
|
||||
albums: make(map[string]*common.PlaylistAlbum),
|
||||
pixbufs: make(map[string]*gdkpixbuf.Pixbuf),
|
||||
}
|
||||
panel.loadWidgets()
|
||||
panel.connectSignals()
|
||||
|
||||
return &panel
|
||||
}
|
||||
|
||||
func (p *playlistPanel) loadWidgets() {
|
||||
builder := gtk.NewBuilderFromString(data.PlaylistPanelXML)
|
||||
|
||||
p.toolbar = builder.GetObject("toolbar").Cast().(*gtk.Box)
|
||||
p.selectButton = builder.GetObject("select_button").Cast().(*gtk.ToggleButton)
|
||||
p.actionbarRevealer = builder.GetObject("actionbar_revealer").Cast().(*gtk.Revealer)
|
||||
p.clearButton = builder.GetObject("playlist_clear_button").Cast().(*gtk.Button)
|
||||
|
||||
p.Bin = builder.GetObject("McgPlaylistPanel").Cast().(*adw.Bin)
|
||||
p.playlistGrid = builder.GetObject("playlist_grid").Cast().(*gtk.GridView)
|
||||
p.playlistModel = gtk.NewStringList(nil)
|
||||
p.selectionMulti = gtk.NewMultiSelection(p.playlistModel)
|
||||
p.selectionSingle = gtk.NewSingleSelection(p.playlistModel)
|
||||
p.playlistGrid.SetModel(p.selectionSingle)
|
||||
|
||||
p.gridFactory = newPictureItemFactory[string](p.itemToModelID, p.modelIDToValue)
|
||||
p.playlistGrid.SetFactory(p.gridFactory.ListItemFactory())
|
||||
|
||||
// Headerbar
|
||||
p.headerbarStandalone = newAlbumHeaderbar()
|
||||
p.playlistStack = builder.GetObject("playlist_stack").Cast().(*gtk.Stack)
|
||||
p.panelNormal = builder.GetObject("panel_normal").Cast().(*gtk.Box)
|
||||
p.panelStandalone = builder.GetObject("panel_standalone").Cast().(*gtk.Box)
|
||||
|
||||
p.standaloneStack = builder.GetObject("standalone_stack").Cast().(*gtk.Stack)
|
||||
p.standaloneSpinner = builder.GetObject("standalone_spinner").Cast().(*gtk.Spinner)
|
||||
p.standaloneScroll = builder.GetObject("standalone_scroll").Cast().(*gtk.ScrolledWindow)
|
||||
p.standaloneImage = builder.GetObject("standalone_image").Cast().(*gtk.Image)
|
||||
p.standalonePlayButton = builder.GetObject("standalone_play_button").Cast().(*gtk.Button)
|
||||
p.standaloneRemoveButton = builder.GetObject("standalone_remove_button").Cast().(*gtk.Button)
|
||||
|
||||
// Selection
|
||||
p.selectionRemoveButton = builder.GetObject("selection_remove_button").Cast().(*gtk.Button)
|
||||
p.selectionCancelButton = builder.GetObject("selection_cancel_button").Cast().(*gtk.Button)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) itemToModelID(item *glib.Object) string {
|
||||
return item.Cast().(*gtk.StringObject).String()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) modelIDToValue(itemID string) (tooltip string, pixbuf *gdkpixbuf.Pixbuf) {
|
||||
if album, ok := p.albums[itemID]; ok {
|
||||
tooltip = album.Tooltip()
|
||||
}
|
||||
pixbuf = p.pixbufs[itemID]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *playlistPanel) connectSignals() {
|
||||
p.selectButton.ConnectToggled(p.onSelectToggled)
|
||||
p.clearButton.ConnectClicked(p.onClearClicked)
|
||||
|
||||
p.playlistGrid.ConnectActivate(p.onGridClicked)
|
||||
|
||||
p.headerbarStandalone.ConnectClose(p.onHeaderbarStandaloneClose)
|
||||
p.standalonePlayButton.ConnectClicked(p.onStandalonePlayClicked)
|
||||
p.standaloneRemoveButton.ConnectClicked(p.onStandaloneRemoveClicked)
|
||||
|
||||
p.selectionRemoveButton.ConnectClicked(p.onSelectionRemove)
|
||||
p.selectionCancelButton.ConnectClicked(p.onSelectionCancel)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) ConnectStandalone(callback func(bool)) {
|
||||
p.standaloneCallback = callback
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onSelectToggled() {
|
||||
if p.selectButton.Active() {
|
||||
p.actionbarRevealer.SetRevealChild(true)
|
||||
p.playlistGrid.SetModel(p.selectionMulti)
|
||||
p.playlistGrid.SetSingleClickActivate(false)
|
||||
p.playlistGrid.StyleContext().AddClass("selection")
|
||||
} else {
|
||||
p.actionbarRevealer.SetRevealChild(false)
|
||||
p.playlistGrid.SetModel(p.selectionSingle)
|
||||
p.playlistGrid.SetSingleClickActivate(true)
|
||||
p.playlistGrid.StyleContext().AddClass("selection")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onClearClicked() {
|
||||
client.Instance().ClearPlaylist()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onGridClicked(position uint) {
|
||||
// Get selected album
|
||||
itemObj := p.playlistModel.Item(position)
|
||||
if itemObj == nil {
|
||||
return
|
||||
}
|
||||
strObj := itemObj.Cast().(*gtk.StringObject)
|
||||
id := strObj.String()
|
||||
album, found := p.albums[id]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
// Show standalone album
|
||||
if p.playlistGrid.Model().Native() == p.selectionSingle.Native() {
|
||||
p.standaloneAlbum = album
|
||||
p.headerbarStandalone.SetAlbum(album.Album)
|
||||
p.standaloneStack.SetVisibleChild(p.standaloneSpinner)
|
||||
p.standaloneSpinner.Start()
|
||||
p.openStandalone()
|
||||
client.Instance().LoadAlbumart(album.Album)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onHeaderbarStandaloneClose() {
|
||||
p.closeStandalone()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) openStandalone() {
|
||||
p.playlistStack.SetVisibleChild(p.panelStandalone)
|
||||
if p.standaloneCallback != nil {
|
||||
p.standaloneCallback(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playlistPanel) closeStandalone() {
|
||||
p.playlistStack.SetVisibleChild(p.panelNormal)
|
||||
if p.standaloneCallback != nil {
|
||||
p.standaloneCallback(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playlistPanel) setCoverLoader(loader *CoverLoader) {
|
||||
p.coverLoader = loader
|
||||
}
|
||||
|
||||
func (p *playlistPanel) SetAlbumart(album *common.Album, albumart []byte) {
|
||||
if p.standaloneAlbum == nil || album != p.standaloneAlbum.Album {
|
||||
return
|
||||
}
|
||||
|
||||
p.standalonePixbuf = nil
|
||||
if albumart == nil || len(albumart) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
loader, loaderErr := loadPixbuf(albumart)
|
||||
if loaderErr != nil {
|
||||
log.Println("failed to load albumart for ", album.Title(), ":", loaderErr)
|
||||
return
|
||||
}
|
||||
|
||||
p.standalonePixbuf = loader.Pixbuf()
|
||||
glib.IdleAdd(p.showStandaloneImage)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) showStandaloneImage() {
|
||||
p.resizeStandaloneImage()
|
||||
p.standaloneStack.SetVisibleChild(p.standaloneScroll)
|
||||
p.standaloneSpinner.Stop()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) resizeStandaloneImage() {
|
||||
newWidth, newHeight := p.getStandaloneSize()
|
||||
if p.standalonePixbuf == nil {
|
||||
p.standaloneImage.SetPixelSize(min(newWidth, newHeight))
|
||||
return
|
||||
}
|
||||
|
||||
width, height := p.calculateCoverSize(newWidth, newHeight)
|
||||
if width <= 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
scaledPixbuf := p.standalonePixbuf.ScaleSimple(width, height, gdkpixbuf.InterpHyper)
|
||||
p.standaloneImage.SetFromPaintable(gdk.NewTextureForPixbuf(scaledPixbuf))
|
||||
p.standaloneImage.SetPixelSize(min(width, height))
|
||||
}
|
||||
|
||||
func (p *playlistPanel) getStandaloneSize() (int, int) {
|
||||
return p.standaloneStack.Size(gtk.OrientationHorizontal), p.standaloneStack.Size(gtk.OrientationVertical)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) calculateCoverSize(currentWidth, currentHeight int) (width, height int) {
|
||||
ratioWidth := float64(currentWidth) / float64(p.standalonePixbuf.Width())
|
||||
ratioHeight := float64(currentHeight) / float64(p.standalonePixbuf.Height())
|
||||
ratio := min(min(ratioWidth, ratioHeight), 1)
|
||||
|
||||
width = int(math.Floor(float64(p.standalonePixbuf.Width()) * ratio))
|
||||
height = int(math.Floor(float64(p.standalonePixbuf.Height()) * ratio))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onStandalonePlayClicked() {
|
||||
client.Instance().PlayQueuedAlbum(p.standaloneAlbum)
|
||||
p.closeStandalone()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onStandaloneRemoveClicked() {
|
||||
client.Instance().UnqueueAlbum(p.standaloneAlbum)
|
||||
p.closeStandalone()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onSelectionRemove() {
|
||||
albums := p.getSelectedAlbums()
|
||||
if len(albums) > 0 {
|
||||
client.Instance().UnqueueAlbums(albums)
|
||||
}
|
||||
|
||||
p.selectButton.SetActive(false)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) getSelectedAlbums() []*common.PlaylistAlbum {
|
||||
albums := []*common.PlaylistAlbum{}
|
||||
for i := range p.selectionMulti.NItems() {
|
||||
if p.selectionMulti.IsSelected(i) {
|
||||
itemObj := p.selectionMulti.Item(i)
|
||||
if itemObj != nil {
|
||||
itemStr := itemObj.Cast().(*gtk.StringObject)
|
||||
albumID := itemStr.String()
|
||||
album, found := p.albums[albumID]
|
||||
if found {
|
||||
albums = append(albums, album)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return albums
|
||||
}
|
||||
|
||||
func (p *playlistPanel) onSelectionCancel() {
|
||||
p.selectButton.SetActive(false)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) Toolbar() gtk.Widgetter {
|
||||
return p.toolbar
|
||||
}
|
||||
|
||||
func (p *playlistPanel) ItemSizeChanged() {
|
||||
p.redraw()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) redraw() {
|
||||
if len(p.playlist) > 0 {
|
||||
p.SetPlaylist(p.playlist)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playlistPanel) SetPlaylist(playlist []*common.PlaylistAlbum) {
|
||||
go p.setPlaylist(playlist)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) setPlaylist(playlist []*common.PlaylistAlbum) {
|
||||
p.playlist = playlist
|
||||
|
||||
for _, album := range playlist {
|
||||
p.albums[album.ID()] = album
|
||||
if p.coverLoader != nil {
|
||||
p.pixbufs[album.ID()] = p.coverLoader.LoadThumbnail(album.Album)
|
||||
}
|
||||
}
|
||||
|
||||
p.updateGridModel()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) updateGridModel() {
|
||||
p.updateGrid(p.playlist)
|
||||
}
|
||||
|
||||
func (p *playlistPanel) updateGrid(albums []*common.PlaylistAlbum) {
|
||||
glib.IdleAdd(func() {
|
||||
n := p.playlistModel.NItems()
|
||||
if n > 0 {
|
||||
p.playlistModel.Splice(0, n, nil)
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(albums))
|
||||
for _, item := range albums {
|
||||
ids = append(ids, item.ID())
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
p.playlistModel.Splice(0, 0, ids)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *playlistPanel) SetWidth(width int) {
|
||||
p.resizeStandaloneImage()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) ClearPlaylist() {
|
||||
client.Instance().ClearPlaylist()
|
||||
}
|
||||
|
||||
func (p *playlistPanel) HeaderbarStandalone() *albumHeaderbar {
|
||||
return p.headerbarStandalone
|
||||
}
|
||||
115
internal/gui/serverpanel.go
Normal file
115
internal/gui/serverpanel.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
)
|
||||
|
||||
type serverPanel struct {
|
||||
*adw.Bin
|
||||
|
||||
toolbar *gtk.Box
|
||||
statusFile *gtk.Label
|
||||
statusAudio *gtk.Label
|
||||
statusBitrate *gtk.Label
|
||||
statusErrer *gtk.Label
|
||||
statsArtist *gtk.Label
|
||||
statsAlbums *gtk.Label
|
||||
statsSongs *gtk.Label
|
||||
statsDBPlaytime *gtk.Label
|
||||
statsPlaytime *gtk.Label
|
||||
statsUptime *gtk.Label
|
||||
outputDevices *gtk.ListBox
|
||||
|
||||
outputDeviceChangeCB OutputDeviceChangeCB
|
||||
}
|
||||
|
||||
type OutputDeviceChangeCB func(device *common.OutputDevice, enable bool)
|
||||
|
||||
func newServerPanel() *serverPanel {
|
||||
panel := serverPanel{}
|
||||
panel.loadWidgets()
|
||||
|
||||
return &panel
|
||||
}
|
||||
|
||||
func (p *serverPanel) loadWidgets() {
|
||||
builder := gtk.NewBuilderFromString(data.ServerPanelXML)
|
||||
|
||||
p.Bin = builder.GetObject("McgServerPanel").Cast().(*adw.Bin)
|
||||
p.toolbar = builder.GetObject("toolbar").Cast().(*gtk.Box)
|
||||
p.statusFile = builder.GetObject("status_file").Cast().(*gtk.Label)
|
||||
p.statusAudio = builder.GetObject("status_audio").Cast().(*gtk.Label)
|
||||
p.statusBitrate = builder.GetObject("status_bitrate").Cast().(*gtk.Label)
|
||||
p.statusErrer = builder.GetObject("status_error").Cast().(*gtk.Label)
|
||||
p.statsArtist = builder.GetObject("stats_artists").Cast().(*gtk.Label)
|
||||
p.statsAlbums = builder.GetObject("stats_albums").Cast().(*gtk.Label)
|
||||
p.statsSongs = builder.GetObject("stats_songs").Cast().(*gtk.Label)
|
||||
p.statsDBPlaytime = builder.GetObject("stats_dbplaytime").Cast().(*gtk.Label)
|
||||
p.statsPlaytime = builder.GetObject("stats_playtime").Cast().(*gtk.Label)
|
||||
p.statsUptime = builder.GetObject("stats_uptime").Cast().(*gtk.Label)
|
||||
p.outputDevices = builder.GetObject("output_devices").Cast().(*gtk.ListBox)
|
||||
}
|
||||
|
||||
func (p *serverPanel) ReceiveOutputDeviceChange(callback OutputDeviceChangeCB) {
|
||||
p.outputDeviceChangeCB = callback
|
||||
}
|
||||
|
||||
func (p *serverPanel) Toolbar() gtk.Widgetter {
|
||||
return p.toolbar
|
||||
}
|
||||
|
||||
func (p *serverPanel) Status(file, audio, bitrate, errorMessage string) {
|
||||
p.statusFile.SetText(file)
|
||||
p.statusAudio.SetText(p.formatAudio(audio))
|
||||
p.statusBitrate.SetText(p.formatBitrate(bitrate))
|
||||
p.statusErrer.SetText(errorMessage)
|
||||
}
|
||||
|
||||
func (p *serverPanel) formatAudio(audio string) string {
|
||||
parts := strings.Split(audio, ":")
|
||||
if len(parts) == 3 {
|
||||
return fmt.Sprintf("%s Hz, %s bit, %s channels", parts[0], parts[1], parts[2])
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *serverPanel) formatBitrate(bitrate string) string {
|
||||
if bitrate != "" {
|
||||
return bitrate + " kb/s"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *serverPanel) Statistics(artists, albums, songs, dbplaytime, playtime, uptime string) {
|
||||
p.statsArtist.SetText(artists)
|
||||
p.statsAlbums.SetText(albums)
|
||||
p.statsSongs.SetText(songs)
|
||||
p.statsDBPlaytime.SetText(dbplaytime)
|
||||
p.statsPlaytime.SetText(playtime)
|
||||
p.statsUptime.SetText(uptime)
|
||||
}
|
||||
|
||||
func (p *serverPanel) OutputDevices(devices []*common.OutputDevice) {
|
||||
p.outputDevices.RemoveAll()
|
||||
for _, device := range devices {
|
||||
deviceButton := gtk.NewCheckButtonWithLabel(device.Name)
|
||||
if device.Enabled {
|
||||
deviceButton.SetActive(true)
|
||||
}
|
||||
deviceButton.ConnectToggled(func() {
|
||||
if p.outputDeviceChangeCB != nil {
|
||||
p.outputDeviceChangeCB(device, deviceButton.Active())
|
||||
}
|
||||
})
|
||||
|
||||
p.outputDevices.Insert(deviceButton, -1)
|
||||
}
|
||||
}
|
||||
23
internal/gui/shortcutsdialog.go
Normal file
23
internal/gui/shortcutsdialog.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
)
|
||||
|
||||
type shortcutsDialog struct {
|
||||
*gtk.ShortcutsWindow
|
||||
}
|
||||
|
||||
func newShortcutsDialog() *shortcutsDialog {
|
||||
dialog := shortcutsDialog{}
|
||||
dialog.loadWidgets()
|
||||
|
||||
return &dialog
|
||||
}
|
||||
|
||||
func (d *shortcutsDialog) loadWidgets() {
|
||||
builder := gtk.NewBuilderFromString(data.ShortcutsDialogXML)
|
||||
|
||||
d.ShortcutsWindow = builder.GetObject("McgShortcutsDialog").Cast().(*gtk.ShortcutsWindow)
|
||||
}
|
||||
454
internal/gui/window.go
Normal file
454
internal/gui/window.go
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/gio/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"suruatoel.xyz/mcg/data"
|
||||
"suruatoel.xyz/mcg/internal/client"
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
"suruatoel.xyz/mcg/internal/i18n"
|
||||
)
|
||||
|
||||
type Panel interface {
|
||||
Toolbar() gtk.Widgetter
|
||||
}
|
||||
|
||||
type GridPanel interface {
|
||||
HeaderbarStandalone() *albumHeaderbar
|
||||
}
|
||||
|
||||
type mainWindow struct {
|
||||
*adw.ApplicationWindow
|
||||
|
||||
toggleFullscreenAction *gio.SimpleAction
|
||||
playAaction *gio.SimpleAction
|
||||
panelAction *gio.SimpleAction
|
||||
clearPlaylistAction *gio.SimpleAction
|
||||
searchLibraryAction *gio.SimpleAction
|
||||
|
||||
toolbarView *adw.ToolbarView
|
||||
headerBar *adw.HeaderBar
|
||||
connectButton *gtk.Switch
|
||||
connectButtonActive bool
|
||||
playButton *gtk.ToggleButton
|
||||
playButtonActive bool
|
||||
volumeButton *gtk.VolumeButton
|
||||
settingVolume bool
|
||||
toolbarStack *gtk.Stack
|
||||
infoToast *adw.ToastOverlay
|
||||
contentStack *gtk.Stack
|
||||
connectionPanel *connectionPanel
|
||||
panelStack *adw.ViewStack
|
||||
serverPanel *serverPanel
|
||||
coverPanel *coverPanel
|
||||
playlistPanel *playlistPanel
|
||||
libraryPanel *libraryPanel
|
||||
resizeHelper *gtk.DrawingArea
|
||||
|
||||
panels map[string]Panel
|
||||
currentWidth int
|
||||
coverLoader *CoverLoader
|
||||
}
|
||||
|
||||
func newMainWindow(app *gtk.Application, config Configuration) *mainWindow {
|
||||
window := mainWindow{
|
||||
panels: make(map[string]Panel),
|
||||
}
|
||||
window.loadWidgets(app, config)
|
||||
window.connectSignals()
|
||||
window.registerActions()
|
||||
window.SetHelpOverlay(newShortcutsDialog().ShortcutsWindow)
|
||||
|
||||
return &window
|
||||
}
|
||||
|
||||
func (w *mainWindow) loadWidgets(app *gtk.Application, config Configuration) {
|
||||
builder := gtk.NewBuilderFromString(data.WindowXML)
|
||||
w.ApplicationWindow = builder.GetObject("McgAppWindow").Cast().(*adw.ApplicationWindow)
|
||||
w.ApplicationWindow.SetApplication(app)
|
||||
|
||||
w.toolbarView = builder.GetObject("toolbar_view").Cast().(*adw.ToolbarView)
|
||||
w.headerBar = builder.GetObject("headerbar").Cast().(*adw.HeaderBar)
|
||||
|
||||
w.connectButton = builder.GetObject("headerbar_button_connect").Cast().(*gtk.Switch)
|
||||
w.connectButtonActive = true
|
||||
|
||||
w.playButton = builder.GetObject("headerbar_button_playpause").Cast().(*gtk.ToggleButton)
|
||||
w.playButtonActive = true
|
||||
|
||||
w.volumeButton = builder.GetObject("headerbar_button_volume").Cast().(*gtk.VolumeButton)
|
||||
w.volumeButton.ConnectValueChanged(w.OnVolumeChanged)
|
||||
|
||||
w.toolbarStack = builder.GetObject("toolbar_stack").Cast().(*gtk.Stack)
|
||||
|
||||
w.infoToast = builder.GetObject("info_toast").Cast().(*adw.ToastOverlay)
|
||||
|
||||
w.contentStack = builder.GetObject("content_stack").Cast().(*gtk.Stack)
|
||||
|
||||
w.connectionPanel = newConnectionPanel(config)
|
||||
w.contentStack.AddChild(w.connectionPanel)
|
||||
w.contentStack.SetVisibleChild(w.connectionPanel)
|
||||
|
||||
// Server panel
|
||||
w.serverPanel = newServerPanel()
|
||||
w.panels["server"] = w.serverPanel
|
||||
w.serverPanel.ReceiveOutputDeviceChange(w.OnServerPanelOutputDeviceChange)
|
||||
|
||||
// Cover panel
|
||||
w.coverPanel = newCoverPanel()
|
||||
w.panels["cover"] = w.coverPanel
|
||||
|
||||
// Playlist panel
|
||||
w.playlistPanel = newPlaylistPanel()
|
||||
w.panels["playlist"] = w.playlistPanel
|
||||
|
||||
// Library panel
|
||||
w.libraryPanel = newLibraryPanel(config)
|
||||
w.panels["library"] = w.serverPanel
|
||||
|
||||
// Panel stack
|
||||
w.panelStack = builder.GetObject("panel_stack").Cast().(*adw.ViewStack)
|
||||
w.panelStack.AddTitledWithIcon(w.serverPanel, "server", i18n.T("Server"), "network-wired-symbolic")
|
||||
w.panelStack.AddTitledWithIcon(w.coverPanel, "cover", i18n.T("Cover"), "image-x-generic-symbolic")
|
||||
w.panelStack.AddTitledWithIcon(w.playlistPanel, "playlist", i18n.T("Playlist"), "view-list-symbolic")
|
||||
w.panelStack.AddTitledWithIcon(w.libraryPanel, "library", i18n.T("Library"), "emblem-music-symbolic")
|
||||
if _, found := w.panels[config.Panel]; found {
|
||||
w.panelStack.SetVisibleChildName(config.Panel)
|
||||
} else {
|
||||
log.Println("configured initial panel name is unknown:", config.Panel)
|
||||
}
|
||||
|
||||
// Toolbar stack
|
||||
w.toolbarStack.AddNamed(w.serverPanel.Toolbar(), "server")
|
||||
w.toolbarStack.AddNamed(w.coverPanel.Toolbar(), "cover")
|
||||
w.toolbarStack.AddNamed(w.playlistPanel.Toolbar(), "playlist")
|
||||
w.toolbarStack.AddNamed(w.libraryPanel.Toolbar(), "library")
|
||||
w.toolbarStack.SetVisibleChildName(config.Panel)
|
||||
|
||||
w.resizeHelper = builder.GetObject("resize_helper").Cast().(*gtk.DrawingArea)
|
||||
}
|
||||
|
||||
func (w *mainWindow) connectSignals() {
|
||||
w.connectButton.Connect("notify::active", func() {
|
||||
w.connect()
|
||||
})
|
||||
w.connectButton.ConnectStateSet(w.OnConnectStateSet)
|
||||
|
||||
w.playButton.ConnectToggled(w.OnPlayPauseToggled)
|
||||
|
||||
w.panelStack.Connect("notify::visible-child", w.OnPanelStackSwitched)
|
||||
w.resizeHelper.ConnectResize(w.OnWindowResize)
|
||||
|
||||
w.coverPanel.ConnectFullscreen(w.OnCoverPanelFullscreen)
|
||||
w.Connect("notify::fullscreened", w.OnWindowFullscreen)
|
||||
|
||||
w.playlistPanel.ConnectStandalone(w.OnPlaylistPanelStandalone)
|
||||
|
||||
w.libraryPanel.ConnectItemSizeChanged(w.onLibraryPanelItemSizeChanged)
|
||||
w.libraryPanel.ConnectStandalone(w.OnLibraryPanelStandalone)
|
||||
|
||||
clientInstance := client.Instance()
|
||||
clientInstance.ReceiveError(w.OnClientError)
|
||||
clientInstance.RegisterConnection(w.OnClientConnection)
|
||||
clientInstance.ReceiveStatus(w.OnClientStatus)
|
||||
clientInstance.ReceiveStatistics(w.OnClientStatistics)
|
||||
clientInstance.ReceiveOutputDevices(w.OnClientOutputDevces)
|
||||
clientInstance.ReceivePlaylist(w.OnClientPlaylist)
|
||||
clientInstance.ReceiveAlbums(w.OnClientAlbums)
|
||||
clientInstance.ReceiveAlbumart(w.OnClientAlbumart)
|
||||
}
|
||||
|
||||
func (w *mainWindow) registerActions() {
|
||||
connectAction := gio.NewSimpleAction("connect", nil)
|
||||
connectAction.ConnectActivate(func(_ *glib.Variant) {
|
||||
w.connect()
|
||||
})
|
||||
w.AddAction(connectAction)
|
||||
|
||||
w.playAaction = gio.NewSimpleAction("play", nil)
|
||||
w.playAaction.SetEnabled(false)
|
||||
w.playAaction.ConnectActivate(func(_ *glib.Variant) {
|
||||
w.OnPlayPauseToggled()
|
||||
})
|
||||
w.AddAction(w.playAaction)
|
||||
|
||||
w.clearPlaylistAction = gio.NewSimpleAction("clear-playlist", nil)
|
||||
w.clearPlaylistAction.SetEnabled(false)
|
||||
w.clearPlaylistAction.ConnectActivate(func(_ *glib.Variant) {
|
||||
w.playlistPanel.ClearPlaylist()
|
||||
})
|
||||
w.AddAction(w.clearPlaylistAction)
|
||||
|
||||
panelVariant := glib.NewVariantString("server")
|
||||
w.panelAction = gio.NewSimpleActionStateful("panel", panelVariant.Type(), panelVariant)
|
||||
w.panelAction.SetEnabled(false)
|
||||
w.panelAction.ConnectActivate(func(value *glib.Variant) {
|
||||
panelName := value.String()
|
||||
w.panelStack.SetVisibleChildName(panelName)
|
||||
})
|
||||
w.AddAction(w.panelAction)
|
||||
|
||||
w.toggleFullscreenAction = gio.NewSimpleAction("toggle-fullscreen", nil)
|
||||
w.toggleFullscreenAction.SetEnabled(false)
|
||||
w.toggleFullscreenAction.ConnectActivate(func(_ *glib.Variant) {
|
||||
w.Fullscreen()
|
||||
})
|
||||
w.AddAction(w.toggleFullscreenAction)
|
||||
|
||||
w.searchLibraryAction = gio.NewSimpleAction("search-library", nil)
|
||||
w.searchLibraryAction.SetEnabled(false)
|
||||
w.searchLibraryAction.ConnectActivate(func(_ *glib.Variant) {
|
||||
w.panelStack.SetVisibleChild(w.libraryPanel)
|
||||
w.libraryPanel.ShowSearch()
|
||||
})
|
||||
w.AddAction(w.searchLibraryAction)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnPanelStackSwitched() {
|
||||
w.setVisibleToolbar()
|
||||
}
|
||||
|
||||
func (w *mainWindow) setVisibleToolbar() {
|
||||
name := w.panelStack.VisibleChildName()
|
||||
w.toolbarStack.SetVisibleChildName(name)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnWindowFullscreen() {
|
||||
w.reactToFullscreen()
|
||||
}
|
||||
|
||||
func (w *mainWindow) reactToFullscreen() {
|
||||
var cursor *gdk.Cursor
|
||||
if w.IsFullscreen() {
|
||||
w.panelStack.SetVisibleChild(w.coverPanel)
|
||||
w.headerBar.SetVisible(false)
|
||||
w.coverPanel.Fullscreen(true)
|
||||
cursor = gdk.NewCursorFromName("none", nil)
|
||||
} else {
|
||||
w.headerBar.SetVisible(true)
|
||||
w.coverPanel.Fullscreen(false)
|
||||
cursor = gdk.NewCursorFromName("default", nil)
|
||||
}
|
||||
|
||||
w.SetCursor(cursor)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnCoverPanelFullscreen() {
|
||||
if w.IsFullscreen() {
|
||||
w.Unfullscreen()
|
||||
} else {
|
||||
w.Fullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnPlaylistPanelStandalone(open bool) {
|
||||
w.onPanelStandalone(w.playlistPanel, open)
|
||||
}
|
||||
|
||||
func (w *mainWindow) onLibraryPanelItemSizeChanged(itemSize int) {
|
||||
w.coverLoader.SetSize(itemSize)
|
||||
w.playlistPanel.ItemSizeChanged()
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnLibraryPanelStandalone(open bool) {
|
||||
w.onPanelStandalone(w.libraryPanel, open)
|
||||
}
|
||||
|
||||
func (w *mainWindow) onPanelStandalone(panel GridPanel, open bool) {
|
||||
if open {
|
||||
w.toolbarView.AddTopBar(panel.HeaderbarStandalone())
|
||||
w.toolbarView.Remove(w.headerBar)
|
||||
} else {
|
||||
w.toolbarView.AddTopBar(w.headerBar)
|
||||
w.toolbarView.Remove(panel.HeaderbarStandalone())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) connect() {
|
||||
if !w.connectButtonActive {
|
||||
return
|
||||
}
|
||||
|
||||
if client.Instance().IsConnected() {
|
||||
w.libraryPanel.Wait()
|
||||
client.Instance().Disconnect()
|
||||
} else {
|
||||
client.Instance().Connect(w.connectionPanel.ConnectionDetails())
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientConnection(connected bool) {
|
||||
glib.IdleAdd(func() {
|
||||
w.connectButtonActive = false
|
||||
w.connectButton.SetActive(connected)
|
||||
w.connectButton.SetState(connected)
|
||||
w.connectButtonActive = true
|
||||
|
||||
w.playButton.SetSensitive(connected)
|
||||
w.volumeButton.SetSensitive(connected)
|
||||
w.playAaction.SetEnabled(connected)
|
||||
w.panelAction.SetEnabled(connected)
|
||||
if connected {
|
||||
w.contentStack.SetVisibleChild(w.panelStack)
|
||||
} else {
|
||||
w.contentStack.SetVisibleChild(w.connectionPanel)
|
||||
}
|
||||
|
||||
w.clearPlaylistAction.SetEnabled(connected)
|
||||
w.searchLibraryAction.SetEnabled(connected)
|
||||
})
|
||||
|
||||
host, _, _ := w.connectionPanel.ConnectionDetails()
|
||||
w.coverLoader = NewCoverLoader(host)
|
||||
w.playlistPanel.setCoverLoader(w.coverLoader)
|
||||
w.libraryPanel.setCoverLoader(w.coverLoader)
|
||||
}
|
||||
|
||||
func (w *mainWindow) SetConfigError(err error) {
|
||||
glib.IdleAdd(func() {
|
||||
w.infoToast.AddToast(adw.NewToast(fmt.Sprintf("Loading the configuration has failed: %s", err.Error())))
|
||||
})
|
||||
}
|
||||
|
||||
func (w *mainWindow) UpdateConfig(config *Configuration) {
|
||||
config.Panel = w.panelStack.VisibleChildName()
|
||||
|
||||
w.connectionPanel.UpdateConfig(config)
|
||||
w.libraryPanel.UpdateConfig(config)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientError(message string) {
|
||||
glib.IdleAdd(func() {
|
||||
w.infoToast.AddToast(adw.NewToast(message))
|
||||
})
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientStatus(state string, album *common.PlaylistAlbum, pos uint64, time uint64, volume int64, file string, audio string, bitrate string, errorMessage string) {
|
||||
// Album
|
||||
glib.IdleAdd(func() {
|
||||
w.coverPanel.SetAlbum(state, album)
|
||||
})
|
||||
w.toggleFullscreenAction.SetEnabled(album != nil)
|
||||
|
||||
// Fullscreen
|
||||
if w.IsFullscreen() && album == nil {
|
||||
w.Unfullscreen()
|
||||
}
|
||||
|
||||
// State
|
||||
if state == "play" {
|
||||
glib.IdleAdd(func() {
|
||||
w.setPlayPause(true)
|
||||
})
|
||||
glib.IdleAdd(func() {
|
||||
w.coverPanel.SetPlay(pos, time)
|
||||
})
|
||||
} else if state == "pause" || state == "stop" {
|
||||
glib.IdleAdd(func() {
|
||||
w.setPlayPause(false)
|
||||
})
|
||||
w.coverPanel.SetPause()
|
||||
}
|
||||
// Volume
|
||||
glib.IdleAdd(func() {
|
||||
w.setVolume(volume)
|
||||
})
|
||||
|
||||
glib.IdleAdd(func() {
|
||||
w.serverPanel.Status(file, audio, bitrate, errorMessage)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientStatistics(artists, albums, songs, dbplaytime, playtime, uptime string) {
|
||||
glib.IdleAdd(func() {
|
||||
w.serverPanel.Statistics(artists, albums, songs, dbplaytime, playtime, uptime)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientOutputDevces(devices []*common.OutputDevice) {
|
||||
glib.IdleAdd(func() {
|
||||
w.serverPanel.OutputDevices(devices)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientPlaylist(playlist []*common.PlaylistAlbum) {
|
||||
w.playlistPanel.SetPlaylist(playlist)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientAlbums(albums map[string]*common.Album) {
|
||||
w.libraryPanel.SetAlbums(albums)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnClientAlbumart(album *common.Album, albumart []byte) {
|
||||
w.coverPanel.SetAlbumart(album, albumart)
|
||||
w.playlistPanel.SetAlbumart(album, albumart)
|
||||
w.libraryPanel.SetAlbumart(album, albumart)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnConnectStateSet(state bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnPlayPauseToggled() {
|
||||
if w.playButtonActive {
|
||||
client.Instance().PlayPause()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnVolumeChanged(value float64) {
|
||||
if !w.settingVolume {
|
||||
client.Instance().SetVolume(int(value * 100))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) setPlayPause(play bool) {
|
||||
w.playButtonActive = false
|
||||
w.playButton.SetActive(play)
|
||||
w.playButtonActive = true
|
||||
}
|
||||
|
||||
func (w *mainWindow) setVolume(volume int64) {
|
||||
if volume >= 0 {
|
||||
w.volumeButton.SetVisible(true)
|
||||
w.settingVolume = true
|
||||
w.volumeButton.SetValue(float64(volume) / 100.0)
|
||||
w.settingVolume = false
|
||||
} else {
|
||||
w.volumeButton.SetVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnServerPanelOutputDeviceChange(device *common.OutputDevice, enable bool) {
|
||||
client.Instance().SetOutputDeviceState(device, enable)
|
||||
}
|
||||
|
||||
func (w *mainWindow) OnWindowResize(width int, _ int) {
|
||||
if !w.hasWidthChanged(width) {
|
||||
return
|
||||
}
|
||||
w.setWidth(width)
|
||||
|
||||
if width > 0 {
|
||||
w.updateUIWidth()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mainWindow) hasWidthChanged(width int) bool {
|
||||
return width != w.currentWidth
|
||||
}
|
||||
|
||||
func (w *mainWindow) setWidth(width int) {
|
||||
w.currentWidth = width
|
||||
}
|
||||
|
||||
func (w *mainWindow) updateUIWidth() {
|
||||
w.coverPanel.SetWidth(w.currentWidth)
|
||||
w.playlistPanel.SetWidth(w.currentWidth)
|
||||
w.libraryPanel.SetWidth(w.currentWidth)
|
||||
}
|
||||
9
main.go
9
main.go
|
|
@ -1,9 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"suruatoel.xyz/mcg/internal/gui"
|
||||
"suruatoel.xyz/mcg/internal/i18n"
|
||||
)
|
||||
|
||||
func main() {
|
||||
i18n.Init("mcg", "locale")
|
||||
|
||||
app := gui.NewApplication()
|
||||
exitCode := app.Run(os.Args)
|
||||
if exitCode > 0 {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue