Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e49362256d | |||
| 690f0f8e9a | |||
| b7c0307f6b | |||
| d1bb505e0a | |||
| 4a11ea5ad0 | |||
| af4fc2fb64 | |||
| 377e57ee43 | |||
| 345e7697ff | |||
| 7d474598e3 | |||
| 9b29f7b274 | |||
| 0a109bc886 | |||
| 9311f9974a | |||
| 08cd9dbe65 | |||
| 099adbab8c | |||
| 3d91ab1b35 | |||
| dce1c441a0 | |||
| cd4f32e7f2 | |||
| 79b3111fb0 | |||
| f3e3f920a5 | |||
| 84103229f1 | |||
| ccf68deff7 | |||
| 2b1edc715f | |||
| 4586494f8d | |||
| 188dcb4ef8 | |||
| 99958eabc9 | |||
| bcfd17a8ae | |||
| 420946d7d4 | |||
| 54fe825acf | |||
| 4ebfc12f0e | |||
| a1f8b73590 | |||
| d2e1f6f5d8 | |||
| 75b99e5820 | |||
| 6ba8bc550f | |||
| 21bd0f5832 |
76 changed files with 5689 additions and 6671 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,7 +1 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
/build/
|
||||
/install/
|
||||
/mcg*
|
||||
|
|
|
|||
26
CONTRIBUTING.md
Normal file
26
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Contributing
|
||||
|
||||
There are multiple ways to contribute code to this project:
|
||||
|
||||
* Submit a Pull Request on
|
||||
[https://git.suruatoel.xyz/coderkun/mcg](git.suruatoel.xyz/coderkun/mcg) or
|
||||
[https://codeberg.org/suruatoel/mcg](codeberg.org/suruatoel/mcg)
|
||||
* Send a git pull request via eMail
|
||||
* Send a patch via eMail
|
||||
|
||||
## Internationalization
|
||||
|
||||
Update `.pot` files:
|
||||
|
||||
$ xgettext --language=Go --keyword=T --from-code=UTF-8 --package-name=mcg --output=po/mcg-go.pot $(git ls-files '*.go')
|
||||
$ xgettext --language=Glade --from-code=UTF-8 --output=po/mcg-ui.pot $(git ls-files '*.ui')
|
||||
$ msgcat --use-first -o po/mcg.pot po/mcg-go.pot po/mcg-ui.pot
|
||||
|
||||
Use a tool like [Poedit](https://poedit.net/) to create or update translations
|
||||
in the `locale/` folder.
|
||||
|
||||
## AI Policy
|
||||
|
||||
This project uses a _No AI policy_. Any code you submit to this project has to
|
||||
be written by yourself. If you put this project’s code into any AI tool, you
|
||||
will be banned from this project.
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
h1. Contributing
|
||||
|
||||
There are several ways to participate in this project. This guide covers only the contribution of code (i. e. any change to any file in the repository).
|
||||
|
||||
There are basically two ways to contribute code to this project: Either via GitLab or via e‑mail. The first one is probably easier for most people. However
|
||||
it requires a account at GitLab.com. For people who don’t want to create an account—or have any other reason to not use GitLab—e‑mail is an alternative option.
|
||||
|
||||
h2. Contributing via GitLab
|
||||
|
||||
Create an account on GitLab.com or use your already existing account. Fork this repository inside GitLab, add changes to your fork and finally create a Merge Request to contribute your changes back to this repository.
|
||||
|
||||
h2. Contributing via E‑mail
|
||||
|
||||
To contribute via e‑mail, first clone this repository to your local harddrive. After adding changes to your local copy you have two options to send them back to this repository: as patch or as Pull Request
|
||||
|
||||
h3. E‑mail Patch
|
||||
|
||||
To send a patch with your changes, use git’s @send-email@ option as explained in "Public Project over Email":https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_project_over_email of section "5.2 Distributed Git - Contributing to a Project":https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project of the "Git Pro Book":https://git-scm.com/book/en/v2/. You will find the e‑mail address at "suruatoel.xyz":https://www.suruatoel.xyz.
|
||||
|
||||
h3. E‑mail Pull Request
|
||||
|
||||
Sending a Pull Request via e‑mail requires to publish your copy of the repository (that includes your changes) at a publicly available URL (it is your responsibility to choose a trusted host). Then use git’s @request-pull@ command to send an e‑mail with your pull request. The e‑mail will contain a link to the public repository which can be used to pull your changes from. You will find the e‑mail address at "suruatoel.xyz":https://www.suruatoel.xyz. For more details, please take a look at "Forked Public Project":https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_public_project of section "5.2 Distributed Git - Contributing to a Project":https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project of the "Git Pro Book":https://git-scm.com/book/en/v2.
|
||||
2
LICENSE
2
LICENSE
|
|
@ -1,5 +1,5 @@
|
|||
CoverGrid (mcg) is a client for the Music Player Daemon, focusing on albums instead of single tracks.
|
||||
Copyright ® 2013 – 2017 coderkun <olli@suruatoel.xyz>
|
||||
Copyright ® 2013 – 2026 Olli <olli@suruatoel.xyz>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
50
README.md
50
README.md
|
|
@ -5,59 +5,51 @@ Daemon](http://www.musicpd.org) (MPD), focusing on albums instead of single
|
|||
tracks. It is not intended to be a replacement for your favorite MPD client but
|
||||
an addition to get a better album-experience.
|
||||
|
||||
Website: https://www.suruatoel.xyz/codes/mcg
|
||||
Website: [https://www.suruatoel.xyz/codes/mcg](www.suruatoel.xyz/codes/mcg)
|
||||
|
||||
License: [GPL](http://www.gnu.org/licenses/gpl.html) v3
|
||||
|
||||
Dependencies:
|
||||
|
||||
* [Python](http://www.python.org) 3
|
||||
* [python-dateutil](https://pypi.org/project/python-dateutil/)
|
||||
* [GTK](http://www.gtk.org) 3 (>= 3.22) ([python-gobject](https://live.gnome.org/PyGObject))
|
||||
* [Avahi](http://www.avahi.org) (optional)
|
||||
* [python-keyring](http://pypi.python.org/pypi/keyring) (optional)
|
||||
* [meson](https://mesonbuild.com/) and [ninja](https://ninja-build.org/) (building)
|
||||
* [GTK](http://www.gtk.org) 4 (>= 4.18)
|
||||
* [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/) (>= 1.2)
|
||||
* [go](https://go.dev/) (build only)
|
||||
|
||||
Additionally a [MPD](http://www.musicpd.org) server (version >= 0.21.0) is
|
||||
Additionally, an [MPD](http://www.musicpd.org) server (version >= 0.21.0) is
|
||||
required at runtime.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Build the application with _meson_ and _ninja_:
|
||||
Build the application with _go_:
|
||||
|
||||
$ meson build
|
||||
$ ninja -C build
|
||||
$ go build .
|
||||
|
||||
## Running/Testing
|
||||
## Running
|
||||
|
||||
For testing the application and running it without (system-wide) installation,
|
||||
donwload/clone the code, build it with the `--prefix` option and install it
|
||||
with `ninja`:
|
||||
Run the application for testing purposes:
|
||||
|
||||
$ meson --prefix $(pwd)/install build
|
||||
$ ninja -C build
|
||||
$ ninja -C build install
|
||||
$ go run .
|
||||
|
||||
After that you can run it with _meson’s_ `devenv` command:
|
||||
Run the application:
|
||||
|
||||
$ meson devenv -C build src/mcg
|
||||
$ ./mcg
|
||||
|
||||
## Installing
|
||||
## Packaging
|
||||
|
||||
Install the application system-wide with _meson_ (after building):
|
||||
The suggested package name for this project is “mcg”. To build a production
|
||||
binary, please use the following command:
|
||||
|
||||
$ ninja -C build install
|
||||
$ go build --ldflags="-s -w -X main.Version=$(git describe)" -trimpath ./...
|
||||
|
||||
Note: _On Linux using the distribution’s package manager is the preferred way
|
||||
of installing applications system-wide._
|
||||
In the “data/” folder you will find a `.desktop` file and an SVG icon.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
|
|
|||
43
data/data.go
Normal file
43
data/data.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//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
|
||||
|
||||
//go:embed noise-texture.png
|
||||
var NoiseTexture []byte
|
||||
|
||||
func BuildMainCSS() string {
|
||||
noiseTextureEncoded := "data:image/png;base64," + base64.StdEncoding.EncodeToString(NoiseTexture)
|
||||
|
||||
return strings.ReplaceAll(MainCSS, "@NOISETEXTURE@", noiseTextureEncoded)
|
||||
}
|
||||
47
data/gtk.css
47
data/gtk.css
|
|
@ -1,6 +1,18 @@
|
|||
.bg-texture {
|
||||
#content_stack {
|
||||
box-shadow:inset 4px 4px 10px rgba(0,0,0,0.3);
|
||||
background-image:url('noise-texture.png');
|
||||
background-image:url("@NOISETEXTURE@");
|
||||
}
|
||||
|
||||
#port_spinner {
|
||||
background:none;
|
||||
margin-top:-13px;
|
||||
}
|
||||
#port_spinner text {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#server_stack_sidebar {
|
||||
background-color:alpha(@theme_bg_color, 1);
|
||||
}
|
||||
|
||||
.no-bg {
|
||||
|
|
@ -18,13 +30,13 @@
|
|||
font-weight:bold;
|
||||
}
|
||||
|
||||
revealer.sidebar > * {
|
||||
#cover_info_revealer {
|
||||
background-color:alpha(@theme_bg_color, 0.8);
|
||||
box-shadow:0 0 10px @theme_bg_color;
|
||||
margin-left:20px
|
||||
margin-left:20px;
|
||||
}
|
||||
|
||||
revealer.sidebar scale mark indicator {
|
||||
#cover_info_revealer scale mark indicator {
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
|
|
@ -32,24 +44,13 @@ actionbar {
|
|||
background-color:@theme_unfocused_bg_color;
|
||||
}
|
||||
|
||||
/* Icon View in regular mode */
|
||||
iconview.view:selected,
|
||||
iconview.view:selected:focus {
|
||||
background-color:@theme_selected_bg_color;
|
||||
gridview child {
|
||||
padding: 1px;
|
||||
}
|
||||
iconview.view:hover {
|
||||
-gtk-icon-effect:highlight;
|
||||
gridview.selection child {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Icon View in selection mode */
|
||||
iconview.view.selection {
|
||||
-gtk-icon-effect:dim;
|
||||
}
|
||||
iconview.view.selection:selected,
|
||||
iconview.view.selection:selected:focus {
|
||||
background-color:@theme_selected_bg_color;
|
||||
-gtk-icon-effect:highlight;
|
||||
}
|
||||
iconview.view.selection:hover {
|
||||
-gtk-icon-effect:none;
|
||||
gridview.selection child:hover,
|
||||
gridview.selection child:selected {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
@ -1,61 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.10"/>
|
||||
<template class="McgAlbumHeaderbar" parent="GtkHeaderBar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child type="title">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="standalone_title">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Title</property>
|
||||
<property name="selectable">True</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="scale" value="1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="standalone_artist">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Artist</property>
|
||||
<property name="selectable">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object id="McgAlbumHeaderbar" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_close_clicked" swapped="no"/>
|
||||
<object class="GtkHeaderBar">
|
||||
<child type="title">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="standalone_title">
|
||||
<property name="selectable">True</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="scale" value="1"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="standalone_artist">
|
||||
<property name="selectable">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
<object id="close_button" class="GtkButton">
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">go-previous-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.12"/>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object class="GtkAdjustment" id="server-port-adjustment">
|
||||
<property name="lower">1024</property>
|
||||
<property name="upper">9999</property>
|
||||
|
|
@ -9,174 +9,58 @@
|
|||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">100</property>
|
||||
</object>
|
||||
<template class="McgConnectionPanel" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<object id="McgConnectionPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title">Connect to MPD</property>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=6 -->
|
||||
<object class="GtkGrid">
|
||||
<property name="width-request">500</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="row-homogeneous">True</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<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="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="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<style>
|
||||
<class name="header"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="zeroconf_list">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<signal name="focus-out-event" handler="on_zeroconf_list_outfocused" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<signal name="changed" handler="on_service_selected" swapped="no"/>
|
||||
</object>
|
||||
</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>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<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>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="height">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="host_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="text">localhost</property>
|
||||
<property name="placeholder-text" translatable="yes">Enter hostname or IP address</property>
|
||||
<signal name="focus-out-event" handler="on_host_entry_outfocused" swapped="no"/>
|
||||
<object class="AdwPasswordEntryRow" id="password_row">
|
||||
<property name="title" translatable="yes">Password</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="password_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="placeholder-text" translatable="yes">Enter password or leave blank</property>
|
||||
<property name="input-purpose">password</property>
|
||||
<signal name="focus-out-event" handler="on_password_entry_outfocused" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="port_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="text">6600</property>
|
||||
<property name="input-purpose">number</property>
|
||||
<property name="adjustment">server-port-adjustment</property>
|
||||
<property name="value">6600</property>
|
||||
<signal name="value-changed" handler="on_port_spinner_value_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Host:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Port:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Password:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,187 +1,153 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<template class="McgCoverPanel" parent="GtkOverlay">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object class="GtkBox" id="toolbar">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="fullscreen_button">
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Show the cover in fullscreen mode</property>
|
||||
<property name="action-name">win.toggle-fullscreen</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">view-fullscreen-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object id="McgCoverPanel" class="GtkOverlay">
|
||||
<child>
|
||||
<object class="GtkStack" id="cover_stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="visible-child">cover_default</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="cover_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">cover-spinner</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="cover_default">
|
||||
<property name="icon-name">image-x-generic-symbolic</property>
|
||||
<property name="icon-size">large</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="cover_scroll">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="kinetic-scrolling">False</property>
|
||||
<property name="overlay-scrolling">False</property>
|
||||
<signal name="size-allocate" handler="on_cover_size_allocate" swapped="no"/>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="halign">fill</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="valign">fill</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkViewport" id="cover_box">
|
||||
<property name="name">cover_box</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="halign">fill</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="valign">fill</property>
|
||||
<child>
|
||||
<object class="GtkEventBox" id="cover_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<signal name="button-press-event" handler="on_cover_box_pressed" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="cover_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon_size">0</property>
|
||||
</object>
|
||||
</child>
|
||||
<object class="GtkImage" id="cover_image">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="halign">fill</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="valign">fill</property>
|
||||
<property name="icon-name">image-x-generic-symbolic</property>
|
||||
<property name="icon-size">large</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">cover-scroll</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="index">-1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkRevealer" id="info_revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="transition-type">slide-right</property>
|
||||
<property name="name">cover_info_revealer</property>
|
||||
<style>
|
||||
<class name="background"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="cover_info_scroll">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="vscrollbar-policy">never</property>
|
||||
<property name="max-content-width">200</property>
|
||||
<property name="propagate-natural-width">True</property>
|
||||
<property name="name">cover_info_scroll</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="shadow-type">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">fill</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=3 -->
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="row-spacing">5</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<style>
|
||||
<class name="cover-labels"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkLabel" id="album_title_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label">Album</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="album_date_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label">Date</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="album_artist_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label">Artist</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<style>
|
||||
<class name="cover-labels"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">10</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="songs_scale">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">fill</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="restrict-to-fill-level">False</property>
|
||||
<property name="digits">0</property>
|
||||
<property name="draw-value">False</property>
|
||||
<signal name="button-press-event" handler="on_songs_start_change" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_songs_change" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
|
@ -189,11 +155,7 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="sidebar"/>
|
||||
<class name="background"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.10"/>
|
||||
<template class="McgCoverToolbar" parent="GtkButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="fullscreen_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Show the cover in fullscreen mode</property>
|
||||
<property name="action-name">win.toggle-fullscreen</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">view-fullscreen-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
<property name="non-homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="app-menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.connect</attribute>
|
||||
<attribute name="label" translatable="yes">Connect</attribute>
|
||||
<attribute name="accel"><Primary>c</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.play</attribute>
|
||||
<attribute name="label" translatable="yes">Play</attribute>
|
||||
<attribute name="accel"><Primary>p</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.clear-playlist</attribute>
|
||||
<attribute name="label" translatable="yes">Clear Playlist</attribute>
|
||||
<attribute name="accel"><Primary>r</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.toggle-fullscreen</attribute>
|
||||
<attribute name="label" translatable="yes">Toggle Fullscreen</attribute>
|
||||
<attribute name="accel">F11</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.search-library</attribute>
|
||||
<attribute name="label" translatable="yes">Search Library</attribute>
|
||||
<attribute name="accel"><Primary>f</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.panel</attribute>
|
||||
<attribute name="label" translatable="yes">Connection</attribute>
|
||||
<attribute name="target">0</attribute>
|
||||
<attribute name="accel"><Primary>KP_1</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.panel</attribute>
|
||||
<attribute name="label" translatable="yes">Cover</attribute>
|
||||
<attribute name="target">1</attribute>
|
||||
<attribute name="accel"><Primary>KP_2</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.panel</attribute>
|
||||
<attribute name="label" translatable="yes">Playlist</attribute>
|
||||
<attribute name="target">2</attribute>
|
||||
<attribute name="accel"><Primary>KP_3</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.panel</attribute>
|
||||
<attribute name="label" translatable="yes">Library</attribute>
|
||||
<attribute name="target">3</attribute>
|
||||
<attribute name="accel"><Primary>KP_4</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.show-help-overlay</attribute>
|
||||
<attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
|
||||
<attribute name="accel"><Primary>k</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">app.info</attribute>
|
||||
<attribute name="label" translatable="yes">Info</attribute>
|
||||
<attribute name="accel"><Primary>i</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">app.quit</attribute>
|
||||
<attribute name="label" translatable="yes">Quit</attribute>
|
||||
<attribute name="accel"><Primary>q</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.10"/>
|
||||
<template class="McgInfoDialog" parent="GtkAboutDialog">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window-position">center</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<property name="gravity">center</property>
|
||||
<property name="program-name">CoverGrid</property>
|
||||
<property name="version">3.2</property>
|
||||
<property name="comments" translatable="yes">CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.</property>
|
||||
<property name="website">http://www.suruatoel.xyz/codes/mcg</property>
|
||||
<property name="logo-icon-name">xyz.suruatoel.mcg</property>
|
||||
<property name="license-type">gpl-3-0</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,303 +1,288 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<template class="McgLibraryPanel" parent="GtkStack">
|
||||
<property name="visible">True</property>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object class="GtkAdjustment" id="grid_adjustment">
|
||||
<property name="lower">100</property>
|
||||
<property name="upper">1000</property>
|
||||
<property name="value">150</property>
|
||||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">10</property>
|
||||
</object>
|
||||
<object class="GtkPopover" id="toolbar_popover">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-type">slide-left-right</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_normal">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="halign">end</property>
|
||||
<child>
|
||||
<object class="GtkSearchBar" id="filter_bar">
|
||||
<object class="GtkScale" id="grid_scale">
|
||||
<property name="width-request">350</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="app-paintable">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<signal name="notify" handler="on_filter_bar_notify" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="filter_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="placeholder-text" translatable="yes">search library</property>
|
||||
<signal name="search-changed" handler="on_filter_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="adjustment">grid_adjustment</property>
|
||||
<property name="restrict-to-fill-level">False</property>
|
||||
<property name="fill-level">-1</property>
|
||||
<property name="round-digits">0</property>
|
||||
<property name="digits">0</property>
|
||||
<property name="has-origin">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="GtkButton" id="library-toolbar-update">
|
||||
<property name="label" translatable="yes">update library</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<object class="GtkBox" id="library-toolbar-sort">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="progress_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkSeparator">
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="progress_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkProgressBar" id="progress_bar">
|
||||
<property name="width-request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="pulse-step">0</property>
|
||||
<property name="show-text">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page1</property>
|
||||
<property name="title">page1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scroll">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Sort</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="sort_artist">
|
||||
<property name="label" translatable="yes">sort by artist</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="group">sort_year</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="sort_title">
|
||||
<property name="label" translatable="yes">sort by title</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="group">sort_year</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="sort_year">
|
||||
<property name="label" translatable="yes">sort by year</property>
|
||||
<property name="receives-default">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="sort_modified">
|
||||
<property name="label" translatable="yes">sort by modification</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="group">sort_year</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="toolbar_sort_order_button">
|
||||
<property name="label" translatable="yes">sort library descending</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<child>
|
||||
<object class="GtkIconView" id="library_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin">0</property>
|
||||
<property name="item-orientation">horizontal</property>
|
||||
<property name="row-spacing">0</property>
|
||||
<property name="column-spacing">0</property>
|
||||
<property name="tooltip-column">1</property>
|
||||
<property name="item-padding">0</property>
|
||||
<property name="activate-on-single-click">True</property>
|
||||
<signal name="item-activated" handler="on_library_grid_clicked" swapped="no"/>
|
||||
<signal name="selection-changed" handler="on_library_grid_selection_changed" swapped="no"/>
|
||||
<signal name="size-allocate" handler="on_resize" swapped="no"/>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title">page0</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="actionbar_revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-type">none</property>
|
||||
<child>
|
||||
<object class="GtkActionBar" id="library-actionbar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">queue</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_add_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title">page0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
<object class="GtkBox" id="toolbar">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="toolbar_search_bar">
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Search the library</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_standalone">
|
||||
<object class="GtkToggleButton" id="select_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Select multiple albums</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="standalone_stack">
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Settings and actions</property>
|
||||
<property name="popover">toolbar_popover</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
<object id="McgLibraryPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkStack" id="library_stack">
|
||||
<property name="transition-type">slide-left-right</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_normal">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="standalone_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkSearchBar" id="filter_bar">
|
||||
<property name="search-mode-enabled" bind-source="toolbar_search_bar" bind-property="active" bind-flags="sync-create"/>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="filter_entry">
|
||||
<property name="placeholder-text" translatable="yes">search library</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">standalone-spinne</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="standalone_scroll">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="kinetic-scrolling">False</property>
|
||||
<property name="overlay-scrolling">False</property>
|
||||
<signal name="size-allocate" handler="on_standalone_scroll_size_allocate" swapped="no"/>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkBox" id="progress_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="standalone_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">gtk-missing-image</property>
|
||||
<property name="icon_size">6</property>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="halign">center</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="progress_image">
|
||||
<property name="icon-size">large</property>
|
||||
<property name="icon-name">image-x-generic-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkProgressBar" id="progress_bar">
|
||||
<property name="width-request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="pulse-step">0</property>
|
||||
<property name="show-text">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">standalone-scroll</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scroll">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkGridView" id="library_grid">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="single_click_activate">true</property>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="actionbar_revealer">
|
||||
<property name="transition-type">slide-up</property>
|
||||
<child>
|
||||
<object class="GtkActionBar" id="actionbar">
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="selection_cancel_button">
|
||||
<property name="label" translatable="yes">cancel</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="selection_queue_button">
|
||||
<property name="label" translatable="yes">queue</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_standalone">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="standalone_stack">
|
||||
<property name="vexpand">true</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="standalone_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="standalone_scroll">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="kinetic-scrolling">False</property>
|
||||
<property name="overlay-scrolling">False</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="standalone_image">
|
||||
<property name="icon-name">gtk-missing-image</property>
|
||||
<property name="icon-size">large</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkActionBar" id="actionbar_standalone">
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="standalone_play_button">
|
||||
<property name="label" translatable="yes">play</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="standalone_queue_button">
|
||||
<property name="label" translatable="yes">queue</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkActionBar" id="library-standalone-actionbar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">queue</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_queue_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">play</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page1</property>
|
||||
<property name="title">page1</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,248 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkAdjustment" id="grid_adjustment">
|
||||
<property name="lower">100</property>
|
||||
<property name="upper">1000</property>
|
||||
<property name="value">150</property>
|
||||
<property name="step-increment">1</property>
|
||||
<property name="page-increment">10</property>
|
||||
</object>
|
||||
<object class="GtkPopover" id="toolbar_popover">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScale" id="grid_scale">
|
||||
<property name="width-request">350</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="adjustment">grid_adjustment</property>
|
||||
<property name="restrict-to-fill-level">False</property>
|
||||
<property name="fill-level">-1</property>
|
||||
<property name="round-digits">0</property>
|
||||
<property name="digits">0</property>
|
||||
<property name="has-origin">False</property>
|
||||
<signal name="button-release-event" handler="on_grid_scale_changed" swapped="no"/>
|
||||
<signal name="change-value" handler="on_grid_scale_change" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="library-toolbar-update">
|
||||
<property name="label">gtk-refresh</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<property name="use-stock">True</property>
|
||||
<signal name="clicked" handler="on_update_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="library-toolbar-sort">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Sort</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="sort_artist">
|
||||
<property name="label" translatable="yes">sort by artist</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<property name="group">sort_year</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="sort_title">
|
||||
<property name="label" translatable="yes">sort by title</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<property name="group">sort_year</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="sort_year">
|
||||
<property name="label" translatable="yes">sort by year</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="sort_modified">
|
||||
<property name="label" translatable="yes">sort by modification</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<property name="group">sort_year</property>
|
||||
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="toolbar_sort_order_button">
|
||||
<property name="label">gtk-sort-descending</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="use-stock">True</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="on_sort_order_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<template class="McgLibraryToolbar" parent="GtkButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="toolbar_search_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Search the library</property>
|
||||
<signal name="toggled" handler="on_search_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">system-search-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
<property name="non-homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="select_button">
|
||||
<property name="visible">True</property>
|
||||
<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="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
<property name="non-homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Settings and actions</property>
|
||||
<property name="popover">toolbar_popover</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
<property name="non-homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,194 +1,140 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<template class="McgPlaylistPanel" parent="GtkStack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-type">slide-left-right</property>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object class="GtkBox" id="toolbar">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_normal">
|
||||
<object class="GtkToggleButton" id="select_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Select multiple albums</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="playlist_clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Clear the playlist</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object id="McgPlaylistPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkStack" id="playlist_stack">
|
||||
<property name="transition-type">slide-left-right</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_normal">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkIconView" id="playlist_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin">0</property>
|
||||
<property name="item-orientation">horizontal</property>
|
||||
<property name="row-spacing">0</property>
|
||||
<property name="column-spacing">0</property>
|
||||
<property name="tooltip-column">1</property>
|
||||
<property name="item-padding">0</property>
|
||||
<property name="activate-on-single-click">True</property>
|
||||
<signal name="item-activated" handler="on_playlist_grid_clicked" swapped="no"/>
|
||||
<signal name="selection-changed" handler="on_playlist_grid_selection_changed" swapped="no"/>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkGridView" id="playlist_grid">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="single-click-activate">true</property>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="actionbar_revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-type">slide-up</property>
|
||||
<child>
|
||||
<object class="GtkActionBar" id="actionbar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkRevealer" id="actionbar_revealer">
|
||||
<property name="transition-type">slide-up</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_selection_remove_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page2</property>
|
||||
<property name="title">page2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="panel_standalone">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="standalone_stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="standalone_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">standalone-spinne</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="standalone_scroll">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="kinetic-scrolling">False</property>
|
||||
<property name="overlay-scrolling">False</property>
|
||||
<signal name="size-allocate" handler="on_standalone_scroll_size_allocate" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="standalone_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">gtk-missing-image</property>
|
||||
<property name="icon_size">6</property>
|
||||
<object class="GtkActionBar" id="actionbar">
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="selection_cancel_button">
|
||||
<property name="label" translatable="yes">cancel</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="selection_remove_button">
|
||||
<property name="label" translatable="yes">remove</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">standalone-scroll</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkActionBar" id="actionbar_standalone">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkBox" id="panel_standalone">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_remove_clicked" swapped="no"/>
|
||||
<object class="GtkStack" id="standalone_stack">
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="GtkSpinner" id="standalone_spinner">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="standalone_scroll">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="kinetic-scrolling">False</property>
|
||||
<property name="overlay-scrolling">False</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="standalone_image">
|
||||
<property name="icon-name">gtk-missing-image</property>
|
||||
<property name="icon-size">large</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">play</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
|
||||
<object class="GtkActionBar" id="actionbar_standalone">
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="standalone_play_button">
|
||||
<property name="label" translatable="yes">play</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="standalone_remove_button">
|
||||
<property name="label" translatable="yes">remove</property>
|
||||
<property name="receives-default">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page1</property>
|
||||
<property name="title">page1</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.10"/>
|
||||
<template class="McgPlaylistToolbar" parent="GtkButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="select_button">
|
||||
<property name="visible">True</property>
|
||||
<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="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
<property name="non-homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="playlist_clear_button">
|
||||
<property name="visible">True</property>
|
||||
<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>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
<property name="non-homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,547 +1,326 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<template class="McgServerPanel" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object class="GtkBox" id="toolbar">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="halign">end</property>
|
||||
<child>
|
||||
<object class="GtkStackSidebar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stack">stack</property>
|
||||
<object class="GtkToggleButton" id="sidebar_switcher">
|
||||
<property name="icon-name">sidebar-show-symbolic</property>
|
||||
<property name="active">true</property>
|
||||
<property name="visible" bind-source="server_flap" bind-property="folded" bind-flags="sync-create"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object id="McgServerPanel" class="AdwBin">
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<object class="AdwFlap" id="server_flap">
|
||||
<property name="flap-position">end</property>
|
||||
<property name="reveal-flap" bind-source="sidebar_switcher" bind-property="active" bind-flags="sync-create|bidirectional" />
|
||||
<property name="flap">
|
||||
<object class="GtkStackSidebar">
|
||||
<property name="stack">stack</property>
|
||||
<property name="name">server_stack_sidebar</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="separator">
|
||||
<object class="GtkSeparator"/>
|
||||
</property>
|
||||
<property name="content">
|
||||
<object class="GtkStack" id="stack">
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">status</property>
|
||||
<property name="title" translatable="yes">Status</property>
|
||||
<property name="child">
|
||||
<object class="AdwStatusPage">
|
||||
<property name="icon-name">dialog-information-symbolic</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">dialog-information-symbolic</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=4 -->
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">2</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="hexpand">false</property>
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="label" translatable="yes">File:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="label" translatable="yes">Audio:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="label" translatable="yes">Bitrate:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="label" translatable="yes">Error:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_bitrate">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes"><i>none</i></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_audio">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes"><i>none</i></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_file">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes"><i>none</i></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_audio">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes"><i>none</i></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_bitrate">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes"><i>none</i></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_error">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes"><i>none</i></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title" translatable="yes">Status</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">stats</property>
|
||||
<property name="title" translatable="yes">Statistics</property>
|
||||
<property name="child">
|
||||
<object class="AdwStatusPage">
|
||||
<property name="icon-name">starred-symbolic</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">starred-symbolic</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=3 n-rows=7 -->
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">2</property>
|
||||
<property name="column-spacing">5</property>
|
||||
<property name="column-homogeneous">true</property>
|
||||
<property name="hexpand">false</property>
|
||||
<property name="halign">center</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_artists">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<property name="selectable">True</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Artists</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_albums">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Albums</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_songs">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Albums</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Songs</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Artists</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_dbplaytime">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Seconds</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_uptime">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<object class="GtkLabel">
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">4</property>
|
||||
<property name="column-span">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_playtime">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">5</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">4</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Seconds played</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">5</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="stats_uptime">
|
||||
<property name="halign">end</property>
|
||||
<property name="justify">right</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">6</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Seconds running</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">6</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page1</property>
|
||||
<property name="title" translatable="yes">Statistics</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="center">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">audio-speakers-symbolic</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">devices</property>
|
||||
<property name="title" translatable="yes">Audio Devices</property>
|
||||
<property name="child">
|
||||
<object class="AdwStatusPage">
|
||||
<property name="icon-name">audio-speakers-symbolic</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="output_devices">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">false</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="selection-mode">none</property>
|
||||
<style>
|
||||
<class name="no-bg"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page2</property>
|
||||
<property name="title" translatable="yes">Audio Devices</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.10"/>
|
||||
<template class="McgServerToolbar" parent="GtkButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="layout-style">start</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.18"/>
|
||||
<template class="McgShortcutsDialog" parent="GtkShortcutsWindow">
|
||||
<property name="modal">True</property>
|
||||
<requires lib="gtk+" version="4.8"/>
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object id="McgShortcutsDialog" class="GtkShortcutsWindow">
|
||||
<property name="modal">1</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsSection">
|
||||
<property name="visible">1</property>
|
||||
|
|
@ -11,53 +11,45 @@
|
|||
<property name="max-height">10</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="visible">1</property>
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>KP_1</property>
|
||||
<property name="title" translatable="yes">Switch to the Connection panel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>KP_2</property>
|
||||
<property name="title" translatable="yes">Switch to the Cover panel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>KP_3</property>
|
||||
<property name="title" translatable="yes">Switch to the Playlist panel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>KP_4</property>
|
||||
<property name="title" translatable="yes">Switch to the Library panel</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>k</property>
|
||||
<property name="title" translatable="yes">Show the keyboard shortcuts</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>i</property>
|
||||
<property name="title" translatable="yes">Open the info dialog</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>q</property>
|
||||
<property name="title" translatable="yes">Quit the application</property>
|
||||
</object>
|
||||
|
|
@ -66,25 +58,21 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="visible">1</property>
|
||||
<property name="title" translatable="yes">Player</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>c</property>
|
||||
<property name="title" translatable="yes">Connect or disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>p</property>
|
||||
<property name="title" translatable="yes">Switch between play and pause</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>r</property>
|
||||
<property name="title" translatable="yes">Clear the playlist</property>
|
||||
</object>
|
||||
|
|
@ -93,11 +81,9 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="visible">1</property>
|
||||
<property name="title" translatable="yes">Cover Panel</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator">F11</property>
|
||||
<property name="title" translatable="yes">Show the cover in fullscreen mode</property>
|
||||
</object>
|
||||
|
|
@ -106,11 +92,9 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="visible">1</property>
|
||||
<property name="title" translatable="yes">Library Panel</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator"><primary>f</property>
|
||||
<property name="title" translatable="yes">Search the library</property>
|
||||
</object>
|
||||
|
|
@ -119,5 +103,5 @@
|
|||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,229 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="McgAppWindow" parent="GtkApplicationWindow">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">xyz.suruatoel.mcg</property>
|
||||
<signal name="size-allocate" handler="on_resize" swapped="no"/>
|
||||
<signal name="window-state-event" handler="on_state" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="content_stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-duration">100</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="panel_stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title">page0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="index">-1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="GtkRevealer" id="info_revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="baseline-position">top</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<signal name="close" handler="on_info_bar_close" swapped="no"/>
|
||||
<signal name="response" handler="on_info_bar_response" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar" id="headerbar">
|
||||
<property name="name">headerbar</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<child type="title">
|
||||
<object class="GtkStack" id="headerbar_title_stack">
|
||||
<property name="name">headerbar-connection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="transition-duration">100</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="headerbar_connection_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Connect to MPD</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page1</property>
|
||||
<property name="title">page1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackSwitcher" id="headerbar_panel_switcher">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<requires lib="gtk+" version="4.8" />
|
||||
<requires lib="adw" version="1.2" />
|
||||
<object id="McgAppWindow" class="AdwApplicationWindow">
|
||||
<property name="title">CoverGrid</property>
|
||||
<property name="content">
|
||||
<object class="AdwToolbarView" id="toolbar_view">
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar" id="headerbar">
|
||||
<property name="centering-policy">strict</property>
|
||||
<property name="show_end_title_buttons">true</property>
|
||||
<property name="title-widget">
|
||||
<object class="AdwViewSwitcherTitle" id="headerbar_panel_switcher">
|
||||
<property name="title">CoverGrid</property>
|
||||
<property name="stack">panel_stack</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">page0</property>
|
||||
<property name="title">page0</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="headerbar_button_connect">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Connect or disconnect</property>
|
||||
<signal name="notify::active" handler="on_headerbar_connection_active_notify" swapped="no"/>
|
||||
<signal name="state-set" handler="on_headerbar_connection_state_set" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="headerbar_button_playpause">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Switch between play and pause</property>
|
||||
<signal name="toggled" handler="on_headerbar_playpause_toggled" swapped="no"/>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-playback-start</property>
|
||||
<object class="GtkSwitch" id="headerbar_button_connect">
|
||||
</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>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">media-playback-start</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVolumeButton" id="headerbar_button_volume">
|
||||
<property name="sensitive">false</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkStack" id="toolbar_stack">
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVolumeButton" id="headerbar_button_volume">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Adjust the volume</property>
|
||||
<property name="relief">none</property>
|
||||
<property name="content">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<signal name="button-press-event" handler="on_headerbar_volume_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_headerbar_volume_release" swapped="no"/>
|
||||
<signal name="value-changed" handler="on_headerbar_volume_changed" swapped="no"/>
|
||||
<child internal-child="plus_button">
|
||||
<object class="GtkButton">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="relief">none</property>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="minus_button">
|
||||
<object class="GtkButton">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="relief">none</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="toolbar_stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="AdwToastOverlay" id="info_toast">
|
||||
<child>
|
||||
<object class="GtkStack" id="content_stack">
|
||||
<property name="name">content_stack</property>
|
||||
<property name="vexpand">true</property>
|
||||
<child>
|
||||
<object class="AdwViewStack" id="panel_stack">
|
||||
<property name="vexpand">true</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwViewSwitcherBar">
|
||||
<property name="stack">panel_stack</property>
|
||||
<binding name="reveal">
|
||||
<lookup name="title-visible">headerbar_panel_switcher</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkDrawingArea" id="resize_helper">
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="bg-texture"/>
|
||||
</style>
|
||||
</template>
|
||||
</property>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
|
|||
|
|
@ -1,21 +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/gtk.menu.ui</file>
|
||||
<file>ui/info-dialog.ui</file>
|
||||
<file>ui/shortcuts-dialog.ui</file>
|
||||
<file>ui/connection-panel.ui</file>
|
||||
<file>ui/album-headerbar.ui</file>
|
||||
<file>ui/server-toolbar.ui</file>
|
||||
<file>ui/server-panel.ui</file>
|
||||
<file>ui/cover-toolbar.ui</file>
|
||||
<file>ui/cover-panel.ui</file>
|
||||
<file>ui/playlist-toolbar.ui</file>
|
||||
<file>ui/playlist-panel.ui</file>
|
||||
<file>ui/library-toolbar.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 |
12
go.mod
Normal file
12
go.mod
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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
Normal file
6
go.sum
Normal file
|
|
@ -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=
|
||||
947
internal/client/client.go
Normal file
947
internal/client/client.go
Normal file
|
|
@ -0,0 +1,947 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"suruatoel.xyz/mcg/internal/common"
|
||||
)
|
||||
|
||||
type QueueItem struct {
|
||||
Command func(*Client) error
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Host string
|
||||
con *textproto.Conn
|
||||
runGroup sync.WaitGroup
|
||||
queue chan QueueItem
|
||||
idling bool
|
||||
version string
|
||||
state string
|
||||
playlist []*common.PlaylistAlbum
|
||||
albums map[string]*common.Album
|
||||
errorCB ErrorCB
|
||||
connectionCB ConnectionCB
|
||||
playlistCB PlaylistCB
|
||||
albumsCB AlbumsCB
|
||||
statusCB StatusCB
|
||||
statisticsCB StatisticsCB
|
||||
outputDevicesCB OutputDevicesCB
|
||||
albumartCB AlbumartCB
|
||||
albumartChan chan []byte
|
||||
}
|
||||
|
||||
type ErrorCB func(string)
|
||||
|
||||
type ConnectionCB func(bool)
|
||||
|
||||
type PlaylistCB func([]*common.PlaylistAlbum)
|
||||
|
||||
type AlbumsCB func(map[string]*common.Album)
|
||||
|
||||
type StatusCB func(string, *common.PlaylistAlbum, uint64, uint64, int64, string, string, string, string)
|
||||
|
||||
type StatisticsCB func(string, string, string, string, string, string)
|
||||
|
||||
type OutputDevicesCB func(devices []*common.OutputDevice)
|
||||
|
||||
type AlbumartCB func(*common.Album, []byte)
|
||||
|
||||
var (
|
||||
clientInstance *Client
|
||||
clientInit sync.Once
|
||||
)
|
||||
|
||||
func Instance() *Client {
|
||||
clientInit.Do(func() {
|
||||
clientInstance = &Client{}
|
||||
})
|
||||
|
||||
return clientInstance
|
||||
}
|
||||
|
||||
func (c *Client) IsConnected() bool {
|
||||
return (c.con != nil)
|
||||
}
|
||||
|
||||
func (c *Client) Connect(host string, port int, password string) {
|
||||
c.Host = host
|
||||
c.queue = make(chan QueueItem, 300)
|
||||
c.albumartChan = make(chan []byte)
|
||||
// Add connect action
|
||||
c.addAction(
|
||||
func(client *Client) error {
|
||||
return client.connect(host, port, password)
|
||||
},
|
||||
)
|
||||
|
||||
go c.run()
|
||||
}
|
||||
|
||||
func (c *Client) connect(host string, port int, password string) error {
|
||||
// Notify listeners
|
||||
defer func() {
|
||||
if c.connectionCB != nil {
|
||||
c.connectionCB(c.con != nil)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create socket
|
||||
con, err := textproto.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
c.con = con
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Greeting
|
||||
greeting, err := c.readLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if greeting[0:6] != "OK MPD" {
|
||||
return textproto.ProtocolError(fmt.Sprintf("Unexpected greeting: %s", greeting))
|
||||
}
|
||||
|
||||
// Parse and store server version
|
||||
c.version = greeting[7:]
|
||||
|
||||
// Add post-connect actions
|
||||
c.addPostConnectActions(password)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) addPostConnectActions(password string) {
|
||||
c.addAction(
|
||||
func(client *Client) error {
|
||||
return client.password(password)
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.loadAlbums()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.loadPlaylist()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.getStatus()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.getStatistics()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.getOutputDevices()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) password(password string) error {
|
||||
if password != "" {
|
||||
reader := &emptyReader{}
|
||||
return c.cmd(reader, "password", password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Disconnect() {
|
||||
c.addDisconnectActions()
|
||||
|
||||
close(c.queue)
|
||||
close(c.albumartChan)
|
||||
c.runGroup.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) addDisconnectActions() {
|
||||
c.addAction(
|
||||
func(client *Client) error {
|
||||
return client.disconnect()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) disconnect() error {
|
||||
// Stop current idle process
|
||||
c.noIdle()
|
||||
|
||||
// Close connection
|
||||
err := c.con.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.con = nil
|
||||
|
||||
// Notify callback
|
||||
if c.connectionCB != nil {
|
||||
c.connectionCB(false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) run() {
|
||||
c.runGroup.Add(1)
|
||||
defer func() {
|
||||
c.runGroup.Done()
|
||||
}()
|
||||
|
||||
for action := range c.queue {
|
||||
err := action.Command(c)
|
||||
if err != nil && c.errorCB != nil {
|
||||
c.errorCB(fmt.Sprintf("Error running action: %s\n", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Version() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveError(callback ErrorCB) {
|
||||
c.errorCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) RegisterConnection(callback ConnectionCB) {
|
||||
c.connectionCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) ReceivePlaylist(callback PlaylistCB) {
|
||||
c.playlistCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveAlbums(callback AlbumsCB) {
|
||||
c.albumsCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveStatus(callback StatusCB) {
|
||||
c.statusCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveStatistics(callback StatisticsCB) {
|
||||
c.statisticsCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveOutputDevices(callback OutputDevicesCB) {
|
||||
c.outputDevicesCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveAlbumart(callback AlbumartCB) {
|
||||
c.albumartCB = callback
|
||||
}
|
||||
|
||||
func (c *Client) loadPlaylist() error {
|
||||
c.playlist = []*common.PlaylistAlbum{}
|
||||
|
||||
playlistReader := &listMapReader{Delimiter: "file"}
|
||||
playlistErr := c.cmd(playlistReader, "playlistinfo")
|
||||
if playlistErr != nil {
|
||||
return playlistErr
|
||||
}
|
||||
|
||||
for _, songData := range playlistReader.DictList() {
|
||||
song := c.extractPlaylistSong(songData)
|
||||
album := c.extractPlaylistAlbum(songData)
|
||||
if len(c.playlist) == 0 || c.playlist[len(c.playlist)-1].ID() != album.ID() {
|
||||
c.playlist = append(c.playlist, album)
|
||||
} else {
|
||||
album = c.playlist[len(c.playlist)-1]
|
||||
}
|
||||
if song != nil {
|
||||
album.AddSong(song)
|
||||
}
|
||||
}
|
||||
|
||||
if c.playlistCB != nil {
|
||||
c.playlistCB(c.playlist)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) loadAlbums() error {
|
||||
// self._callback(Client.SIGNAL_INIT_ALBUMS)
|
||||
c.albums = make(map[string]*common.Album)
|
||||
|
||||
albumReader := &listMapReader{Delimiter: "Album"}
|
||||
albumErr := c.cmd(albumReader, "list album")
|
||||
if albumErr != nil {
|
||||
return albumErr
|
||||
}
|
||||
for _, entry := range albumReader.DictList() {
|
||||
// self._callback(Client.SIGNAL_PULSE_ALBUMS)
|
||||
|
||||
// Album
|
||||
album := c.extractAlbum(entry)
|
||||
if album != nil {
|
||||
// Songs
|
||||
songReader := &listMapReader{Delimiter: "file"}
|
||||
songErr := c.cmd(songReader, "find album", album.Title())
|
||||
if songErr != nil {
|
||||
return songErr
|
||||
}
|
||||
for _, songData := range songReader.DictList() {
|
||||
song := c.extractSong(songData)
|
||||
if song != nil {
|
||||
album.AddSong(song)
|
||||
}
|
||||
}
|
||||
|
||||
c.albums[album.ID()] = album
|
||||
}
|
||||
}
|
||||
|
||||
if c.albumsCB != nil {
|
||||
c.albumsCB(c.albums)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getStatus() error {
|
||||
statusReader := &mapReader{}
|
||||
statusErr := c.cmd(statusReader, "status")
|
||||
if statusErr != nil {
|
||||
return statusErr
|
||||
}
|
||||
|
||||
status := statusReader.Dict()
|
||||
// State
|
||||
c.state = status["state"]
|
||||
// Time
|
||||
var parseErr error
|
||||
time := uint64(0)
|
||||
if timeStr, timeSet := status["time"]; timeSet {
|
||||
time, parseErr = strconv.ParseUint(strings.Split(timeStr, ":")[0], 10, 64)
|
||||
if parseErr != nil {
|
||||
return newValueParseError("time", timeStr)
|
||||
}
|
||||
}
|
||||
// Volume
|
||||
volume := int64(-1)
|
||||
if volumeStr, volumeSet := status["volume"]; volumeSet {
|
||||
volume, parseErr = strconv.ParseInt(volumeStr, 10, 64)
|
||||
if parseErr != nil {
|
||||
return newValueParseError("volume", volumeStr)
|
||||
}
|
||||
}
|
||||
// Error
|
||||
errorMessage := status["error"]
|
||||
|
||||
// Album information
|
||||
file := ""
|
||||
var album *common.PlaylistAlbum
|
||||
pos := uint64(0)
|
||||
songReader := &mapReader{}
|
||||
songErr := c.cmd(songReader, "currentsong")
|
||||
if songErr != nil {
|
||||
return songErr
|
||||
}
|
||||
songData := songReader.Dict()
|
||||
if len(songData) > 0 {
|
||||
// File
|
||||
file = songData["file"]
|
||||
// Song
|
||||
song := c.extractPlaylistSong(songData)
|
||||
if song != nil {
|
||||
// Album
|
||||
album = c.extractPlaylistAlbum(songData)
|
||||
pos = song.Pos()
|
||||
for _, palbum := range c.playlist {
|
||||
if palbum.ID() == album.ID() && uint64(len(palbum.Songs())) >= pos {
|
||||
album = palbum
|
||||
break
|
||||
}
|
||||
pos -= uint64(len(palbum.Songs()))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Audio
|
||||
audio := status["audio"]
|
||||
// Bitrate
|
||||
bitrate := status["bitrate"]
|
||||
|
||||
if c.statusCB != nil {
|
||||
c.statusCB(c.state, album, pos, time, volume, file, audio, bitrate, errorMessage)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getStatistics() error {
|
||||
statisticsReader := &mapReader{}
|
||||
statisticsErr := c.cmd(statisticsReader, "stats")
|
||||
if statisticsErr != nil {
|
||||
return statisticsErr
|
||||
}
|
||||
|
||||
stats := statisticsReader.Dict()
|
||||
artists := stats["artists"]
|
||||
albums := stats["albums"]
|
||||
songs := stats["songs"]
|
||||
dbPlaytime := stats["db_playtime"]
|
||||
playtime := stats["playtime"]
|
||||
uptime := stats["uptime"]
|
||||
|
||||
if c.statisticsCB != nil {
|
||||
c.statisticsCB(artists, albums, songs, dbPlaytime, playtime, uptime)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getOutputDevices() error {
|
||||
devicesReader := &listMapReader{Delimiter: "outputid"}
|
||||
devicesErr := c.cmd(devicesReader, "outputs")
|
||||
if devicesErr != nil {
|
||||
return devicesErr
|
||||
}
|
||||
|
||||
devices := []*common.OutputDevice{}
|
||||
for _, deviceMap := range devicesReader.DictList() {
|
||||
device := common.NewOutputDevice(
|
||||
deviceMap["outputid"],
|
||||
deviceMap["outputname"],
|
||||
)
|
||||
|
||||
enabledString := deviceMap["outputenabled"]
|
||||
if enabledString != "" {
|
||||
enabled, enabledErr := strconv.ParseBool(enabledString)
|
||||
if enabledErr == nil {
|
||||
device.Enabled = enabled
|
||||
} else {
|
||||
log.Println("failed to parse status of output device", device.ID, device.Name)
|
||||
}
|
||||
}
|
||||
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
if c.outputDevicesCB != nil {
|
||||
c.outputDevicesCB(devices)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SetOutputDeviceState(device *common.OutputDevice, enable bool) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.setOutputDeviceState(device, enable)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) setOutputDeviceState(device *common.OutputDevice, enable bool) error {
|
||||
reader := &emptyReader{}
|
||||
if enable {
|
||||
return c.cmd(reader, "enableoutput", device.ID)
|
||||
}
|
||||
return c.cmd(reader, "disableoutput", device.ID)
|
||||
}
|
||||
|
||||
func (c *Client) PlayPause() {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.playPause()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) playPause() error {
|
||||
reader := &emptyReader{}
|
||||
switch c.state {
|
||||
case "play":
|
||||
return c.cmd(reader, "pause")
|
||||
default:
|
||||
return c.cmd(reader, "play")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) PlayAlbum(album *common.Album) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.playAlbum(album)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) playAlbum(album *common.Album) error {
|
||||
songIDs, err := c.queueAlbum(album)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(songIDs) > 0 {
|
||||
reader := &emptyReader{}
|
||||
return c.cmd(reader, "playid", songIDs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) QueueAlbum(album *common.Album) {
|
||||
c.addAction(func(client *Client) error {
|
||||
_, err := client.queueAlbum(album)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) queueAlbum(album *common.Album) ([]string, error) {
|
||||
songIDs := []string{}
|
||||
for _, song := range album.Songs() {
|
||||
reader := &mapReader{}
|
||||
addErr := c.cmd(reader, "addid", song.File())
|
||||
if addErr != nil {
|
||||
return songIDs, addErr
|
||||
}
|
||||
|
||||
addData := reader.Dict()
|
||||
songID, found := addData["Id"]
|
||||
if found {
|
||||
songIDs = append(songIDs, songID)
|
||||
}
|
||||
}
|
||||
|
||||
return songIDs, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueueAlbums(albums []*common.Album) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.queueAlbums(albums)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) queueAlbums(albums []*common.Album) error {
|
||||
for _, album := range albums {
|
||||
_, queueErr := c.queueAlbum(album)
|
||||
if queueErr != nil {
|
||||
return queueErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PlayQueuedAlbum(album *common.PlaylistAlbum) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.playQueuedAlbum(album)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) playQueuedAlbum(album *common.PlaylistAlbum) error {
|
||||
if len(album.Songs()) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
reader := &emptyReader{}
|
||||
return c.cmd(reader, "playid", album.Songs()[0].ID())
|
||||
}
|
||||
|
||||
func (c *Client) UnqueueAlbum(album *common.PlaylistAlbum) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.unqueueAlbum(album)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) unqueueAlbum(album *common.PlaylistAlbum) error {
|
||||
commands := []listCommand{}
|
||||
for _, song := range album.Songs() {
|
||||
command := newListCommand("deleteid", song.ID())
|
||||
commands = append(commands, command)
|
||||
}
|
||||
|
||||
return c.cmdList(commands)
|
||||
}
|
||||
|
||||
func (c *Client) UnqueueAlbums(albums []*common.PlaylistAlbum) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.unqueueAlbums(albums)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) unqueueAlbums(albums []*common.PlaylistAlbum) error {
|
||||
commands := []listCommand{}
|
||||
for _, album := range albums {
|
||||
for _, song := range album.Songs() {
|
||||
command := newListCommand("deleteid", song.ID())
|
||||
commands = append(commands, command)
|
||||
}
|
||||
}
|
||||
|
||||
return c.cmdList(commands)
|
||||
}
|
||||
|
||||
func (c *Client) Seek(pos int, time uint64) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.seek(pos, time)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) seek(pos int, time uint64) error {
|
||||
reader := &emptyReader{}
|
||||
return c.cmd(reader, "seek", strconv.Itoa(pos), strconv.FormatUint(time, 10))
|
||||
}
|
||||
|
||||
func (c *Client) SetVolume(volume int) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.setVolume(volume)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) setVolume(volume int) error {
|
||||
reader := &emptyReader{}
|
||||
return c.cmd(reader, "setvol", strconv.Itoa(volume))
|
||||
}
|
||||
|
||||
func (c *Client) LoadAlbumart(album *common.Album) {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.loadAlbumart(album)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) LoadAlbumartNow(album *common.Album) []byte {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.loadAlbumartNow(album)
|
||||
})
|
||||
|
||||
return <-c.albumartChan
|
||||
}
|
||||
|
||||
func (c *Client) loadAlbumart(album *common.Album) error {
|
||||
reader := &binaryReader{}
|
||||
reader.size = 1
|
||||
reader.offset = 0
|
||||
|
||||
if c.albumartCB != nil {
|
||||
// We need to wrap this in an anonymous function to make sure the call to reader.Binary() is deferred as well
|
||||
defer func() {
|
||||
c.albumartCB(album, reader.Binary())
|
||||
}()
|
||||
}
|
||||
|
||||
if len(album.Songs()) > 0 {
|
||||
return c.cmd(reader, "albumart", album.Songs()[0].File(), "0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) loadAlbumartNow(album *common.Album) error {
|
||||
reader := &binaryReader{}
|
||||
reader.size = 1
|
||||
reader.offset = 0
|
||||
|
||||
defer func() {
|
||||
c.albumartChan <- reader.Binary()
|
||||
}()
|
||||
|
||||
if len(album.Songs()) > 0 {
|
||||
return c.cmd(reader, "albumart", album.Songs()[0].File(), "0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ClearPlaylist() {
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.clearPlaylist()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) clearPlaylist() error {
|
||||
reader := &emptyReader{}
|
||||
return c.cmd(reader, "clear")
|
||||
}
|
||||
|
||||
func (c *Client) idle() error {
|
||||
reader := &listReader{
|
||||
key: "changed",
|
||||
}
|
||||
|
||||
c.idling = true
|
||||
err := c.cmd(reader, "idle")
|
||||
c.idling = false
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subsystem := range reader.List() {
|
||||
switch subsystem {
|
||||
case "player":
|
||||
c.addAction(
|
||||
func(client *Client) error {
|
||||
return client.loadPlaylist()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.getStatus()
|
||||
},
|
||||
)
|
||||
case "mixer":
|
||||
fallthrough
|
||||
case "output":
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.getStatus()
|
||||
})
|
||||
case "playlist":
|
||||
c.addAction(func(client *Client) error {
|
||||
return client.loadPlaylist()
|
||||
})
|
||||
case "database":
|
||||
fallthrough
|
||||
case "update":
|
||||
c.addAction(
|
||||
func(client *Client) error {
|
||||
return client.loadAlbums()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.loadPlaylist()
|
||||
},
|
||||
func(client *Client) error {
|
||||
return client.getStatus()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) noIdle() error {
|
||||
id, err := c.write("noidle")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.con.StartResponse(id)
|
||||
defer c.con.EndResponse(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) addAction(commands ...func(*Client) error) {
|
||||
for _, command := range commands {
|
||||
c.queue <- QueueItem{
|
||||
command,
|
||||
}
|
||||
}
|
||||
c.queue <- QueueItem{
|
||||
func(client *Client) error {
|
||||
if client.con != nil && len(client.queue) == 0 {
|
||||
return client.idle()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if c.idling {
|
||||
c.noIdle()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) cmd(reader reader, action string, args ...string) error {
|
||||
readingComplete := false
|
||||
for !readingComplete {
|
||||
command := c.constructCommand(action, args...)
|
||||
id, err := c.write(command)
|
||||
if err != nil {
|
||||
return textproto.ProtocolError(fmt.Sprintf("failed to write command \"%s\": %s", action, err))
|
||||
}
|
||||
|
||||
complete, newArgs, err := c.readData(id, reader, args)
|
||||
if err != nil {
|
||||
return textproto.ProtocolError(fmt.Sprintf("failed to read response for command \"%s\": %s", action, err))
|
||||
}
|
||||
readingComplete = complete
|
||||
args = newArgs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) cmdList(commands []listCommand) error {
|
||||
beginReader := &noopReader{}
|
||||
beginErr := c.cmd(beginReader, "command_list_begin")
|
||||
if beginErr != nil {
|
||||
return beginErr
|
||||
}
|
||||
|
||||
commandReader := &noopReader{}
|
||||
for _, command := range commands {
|
||||
commandErr := c.cmd(commandReader, command.Action, command.Args...)
|
||||
if commandErr != nil {
|
||||
return commandErr
|
||||
}
|
||||
}
|
||||
|
||||
endReader := &emptyReader{}
|
||||
return c.cmd(endReader, "command_list_end")
|
||||
}
|
||||
|
||||
func (c *Client) constructCommand(command string, args ...string) string {
|
||||
if len(args) == 0 {
|
||||
return command
|
||||
}
|
||||
|
||||
cleanArgs := c.escapeCommandArguments(args...)
|
||||
joinedArgs := strings.Join(cleanArgs, "\" \"")
|
||||
|
||||
return fmt.Sprintf("%s \"%s\"", command, joinedArgs)
|
||||
}
|
||||
|
||||
func (c *Client) escapeCommandArguments(args ...string) []string {
|
||||
for i, arg := range args {
|
||||
args[i] = strings.ReplaceAll(arg, "\"", "\\\"")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (c *Client) write(command string) (uint, error) {
|
||||
id := c.con.Next()
|
||||
c.con.StartRequest(id)
|
||||
defer c.con.EndRequest(id)
|
||||
|
||||
fmt.Fprintf(c.con.W, "%s", command)
|
||||
c.con.W.WriteByte('\n')
|
||||
err := c.con.W.Flush()
|
||||
if err != nil {
|
||||
return id, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *Client) readData(id uint, reader reader, args []string) (bool, []string, error) {
|
||||
c.con.StartResponse(id)
|
||||
defer c.con.EndResponse(id)
|
||||
|
||||
return reader.ReadData(c, args)
|
||||
}
|
||||
|
||||
func (c *Client) read() ([]string, error) {
|
||||
response := []string{}
|
||||
|
||||
for {
|
||||
line, err := c.readLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if line == "OK" {
|
||||
break
|
||||
}
|
||||
response = append(response, line)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) readLine() (string, error) {
|
||||
line, err := c.con.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.HasPrefix(line, "ACK ") {
|
||||
return "", textproto.ProtocolError(fmt.Sprintf("Unexpected line: %s", line[4:]))
|
||||
}
|
||||
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (c *Client) readBytes(bytes []byte) error {
|
||||
n, err := io.ReadFull(c.con.R, bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(bytes) {
|
||||
return textproto.ProtocolError(fmt.Sprintf("Wrong byte count: %d instead of %d", n, len(bytes)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) extractAlbum(song map[string]string) *common.Album {
|
||||
albumTitle := "Various"
|
||||
if title, titleSet := song["Album"]; titleSet {
|
||||
albumTitle = title
|
||||
}
|
||||
album := common.NewAlbum(albumTitle)
|
||||
|
||||
if albumLookup, albumExists := c.albums[album.ID()]; albumExists {
|
||||
return albumLookup
|
||||
}
|
||||
|
||||
return album
|
||||
}
|
||||
|
||||
func (c *Client) extractPlaylistAlbum(song map[string]string) *common.PlaylistAlbum {
|
||||
albumTitle := "Various"
|
||||
if title, titleSet := song["Album"]; titleSet {
|
||||
albumTitle = title
|
||||
}
|
||||
album := common.NewAlbum(albumTitle)
|
||||
|
||||
if albumLookup, albumExists := c.albums[album.ID()]; albumExists {
|
||||
album = albumLookup
|
||||
}
|
||||
|
||||
playlistAlbum := common.NewPlaylistAlbum(album)
|
||||
|
||||
return playlistAlbum
|
||||
}
|
||||
|
||||
func (c *Client) extractSong(data map[string]string) *common.Song {
|
||||
if artist, artistSet := data["Artist"]; artistSet {
|
||||
if title, titleSet := data["Title"]; titleSet {
|
||||
if file, fileSet := data["file"]; fileSet {
|
||||
song := common.NewSong(artist, title, file)
|
||||
|
||||
if track, isSet := data["Track"]; isSet {
|
||||
song.SetTrack(track)
|
||||
}
|
||||
if timeString, isSet := data["Time"]; isSet {
|
||||
time, timeErr := strconv.ParseUint(timeString, 10, 64)
|
||||
if timeErr == nil {
|
||||
song.SetLength(time)
|
||||
} else {
|
||||
log.Println("failed to parse \"Time\" field of song file", file)
|
||||
}
|
||||
}
|
||||
if date, isSet := data["Date"]; isSet {
|
||||
song.SetDate(date)
|
||||
}
|
||||
if albumartist, isSet := data["Albumartist"]; isSet {
|
||||
song.AddAlbumartist(albumartist)
|
||||
}
|
||||
if lastModifiedString, isSet := data["last-modified"]; isSet {
|
||||
lastModified, dateErr := time.Parse("2006-01-02T15:04:05-0700", lastModifiedString)
|
||||
if dateErr == nil {
|
||||
song.SetLastModified(lastModified)
|
||||
} else {
|
||||
log.Println("failed to parse \"last-modified\" field of song file", file)
|
||||
}
|
||||
}
|
||||
|
||||
return song
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) extractPlaylistSong(data map[string]string) *common.PlaylistSong {
|
||||
song := c.extractSong(data)
|
||||
if song != nil {
|
||||
if id, idSet := data["Id"]; idSet {
|
||||
if posString, posSet := data["Pos"]; posSet {
|
||||
pos, posErr := strconv.ParseUint(posString, 10, 64)
|
||||
if posErr != nil {
|
||||
log.Println("failed to parse \"Pos\" field of playlist song")
|
||||
}
|
||||
|
||||
return common.NewPlaylistSong(song, id, pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
10
internal/client/command.go
Normal file
10
internal/client/command.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package client
|
||||
|
||||
type listCommand struct {
|
||||
Action string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func newListCommand(action string, args ...string) listCommand {
|
||||
return listCommand{action, args}
|
||||
}
|
||||
19
internal/client/errors.go
Normal file
19
internal/client/errors.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package client
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ValueParseErr struct {
|
||||
field string
|
||||
value string
|
||||
}
|
||||
|
||||
func newValueParseError(field string, value string) ValueParseErr {
|
||||
return ValueParseErr{
|
||||
field: field,
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (e ValueParseErr) Error() string {
|
||||
return fmt.Sprintf("failed to parse %s value: %s", e.field, e.value)
|
||||
}
|
||||
236
internal/client/reader.go
Normal file
236
internal/client/reader.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type reader interface {
|
||||
ReadData(client *Client, args []string) (bool, []string, error)
|
||||
}
|
||||
|
||||
type noopReader struct{}
|
||||
|
||||
func (r *noopReader) ReadData(client *Client, args []string) (bool, []string, error) {
|
||||
return true, args, nil
|
||||
}
|
||||
|
||||
type emptyReader struct{}
|
||||
|
||||
func (r *emptyReader) ReadData(client *Client, args []string) (bool, []string, error) {
|
||||
_, err := client.read()
|
||||
|
||||
return true, args, err
|
||||
}
|
||||
|
||||
type listReader struct {
|
||||
key string
|
||||
data []string
|
||||
}
|
||||
|
||||
func (r *listReader) ReadData(client *Client, args []string) (bool, []string, error) {
|
||||
data, err := client.read()
|
||||
if err != nil {
|
||||
return true, args, err
|
||||
}
|
||||
|
||||
r.data = []string{}
|
||||
for _, line := range data {
|
||||
if !strings.HasPrefix(line, r.key+": ") {
|
||||
return true, args, textproto.ProtocolError(fmt.Sprintf("Unexpected line: %s", line))
|
||||
}
|
||||
|
||||
r.data = append(r.data, line[len(r.key)+2:])
|
||||
}
|
||||
|
||||
return true, args, nil
|
||||
}
|
||||
|
||||
func (r *listReader) List() []string {
|
||||
return r.data
|
||||
}
|
||||
|
||||
type mapReader struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (r *mapReader) ReadData(client *Client, args []string) (bool, []string, error) {
|
||||
data, err := client.read()
|
||||
if err != nil {
|
||||
return true, args, err
|
||||
}
|
||||
|
||||
r.data = make(map[string]string)
|
||||
for _, line := range data {
|
||||
parts := strings.Split(line, ": ")
|
||||
if len(parts) < 2 {
|
||||
return true, args, textproto.ProtocolError(fmt.Sprintf("Unexpected line: %s", line))
|
||||
}
|
||||
r.data[parts[0]] = strings.Join(parts[1:], ": ")
|
||||
}
|
||||
|
||||
return true, args, nil
|
||||
}
|
||||
|
||||
func (r *mapReader) Dict() map[string]string {
|
||||
return r.data
|
||||
}
|
||||
|
||||
type listMapReader struct {
|
||||
Delimiter string
|
||||
data []map[string]string
|
||||
}
|
||||
|
||||
func (r *listMapReader) ReadData(client *Client, args []string) (bool, []string, error) {
|
||||
data, err := client.read()
|
||||
if err != nil {
|
||||
return true, args, err
|
||||
}
|
||||
|
||||
r.data = nil
|
||||
var entry map[string]string
|
||||
for _, line := range data {
|
||||
parts := strings.Split(line, ": ")
|
||||
if len(parts) < 2 {
|
||||
return true, args, textproto.ProtocolError(fmt.Sprintf("Unexpected line: %s", line))
|
||||
}
|
||||
|
||||
if parts[0] == r.Delimiter {
|
||||
if entry != nil {
|
||||
r.data = append(r.data, entry)
|
||||
}
|
||||
entry = make(map[string]string)
|
||||
}
|
||||
entry[parts[0]] = strings.Join(parts[1:], ": ")
|
||||
}
|
||||
if entry != nil {
|
||||
r.data = append(r.data, entry)
|
||||
}
|
||||
|
||||
return true, args, nil
|
||||
}
|
||||
|
||||
func (r *listMapReader) DictList() []map[string]string {
|
||||
return r.data
|
||||
}
|
||||
|
||||
type binaryReader struct {
|
||||
data []byte
|
||||
size int
|
||||
offset int
|
||||
}
|
||||
|
||||
func (r *binaryReader) ReadData(client *Client, args []string) (bool, []string, error) {
|
||||
firstLine, dataComplete, firstLineErr := r.readFirstLine(client)
|
||||
if firstLineErr != nil {
|
||||
return true, args, firstLineErr
|
||||
}
|
||||
if dataComplete {
|
||||
return true, args, nil
|
||||
}
|
||||
|
||||
totalSize, sizeErr := r.readSize(firstLine)
|
||||
if sizeErr != nil {
|
||||
return true, args, sizeErr
|
||||
}
|
||||
r.size = totalSize
|
||||
|
||||
currentSize, bytesCountErr := r.readBytesCount(client)
|
||||
if bytesCountErr != nil {
|
||||
return true, args, bytesCountErr
|
||||
}
|
||||
|
||||
r.initData(totalSize)
|
||||
bytesErr := r.readBytes(client, currentSize)
|
||||
if bytesErr != nil {
|
||||
return true, args, bytesErr
|
||||
}
|
||||
|
||||
_, lineBreakErr := client.readLine()
|
||||
if lineBreakErr != nil {
|
||||
return true, args, lineBreakErr
|
||||
}
|
||||
_, lineCompletionErr := client.readLine()
|
||||
if lineCompletionErr != nil {
|
||||
return true, args, lineCompletionErr
|
||||
}
|
||||
|
||||
complete := (r.offset >= r.size)
|
||||
newArgs := r.updateOffsetArgument(args)
|
||||
|
||||
return complete, newArgs, nil
|
||||
}
|
||||
|
||||
func (r *binaryReader) readFirstLine(client *Client) (line string, complete bool, err error) {
|
||||
line, lineErr := client.readLine()
|
||||
if lineErr != nil {
|
||||
return "", true, lineErr
|
||||
}
|
||||
|
||||
// OK here means the binary data is complete
|
||||
if line == "OK" {
|
||||
return "", true, nil
|
||||
}
|
||||
|
||||
return line, false, nil
|
||||
}
|
||||
|
||||
func (r *binaryReader) readSize(line string) (int, error) {
|
||||
if !strings.HasPrefix(line, "size: ") {
|
||||
return 0, textproto.ProtocolError(fmt.Sprintf("Unexpected line: %s", line))
|
||||
}
|
||||
|
||||
parsedSize, err := strconv.Atoi(line[6:])
|
||||
if err != nil {
|
||||
return 0, textproto.ProtocolError(fmt.Sprintf("Invalid size: %s", line[6:]))
|
||||
}
|
||||
|
||||
return parsedSize, nil
|
||||
}
|
||||
|
||||
func (r *binaryReader) readBytesCount(client *Client) (int, error) {
|
||||
line, lineErr := client.readLine()
|
||||
if lineErr != nil {
|
||||
return 0, lineErr
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(line, "binary: ") {
|
||||
return 0, textproto.ProtocolError(fmt.Sprintf("Unexpected line: %s", line))
|
||||
}
|
||||
|
||||
count, countErr := strconv.Atoi(line[8:])
|
||||
if countErr != nil {
|
||||
return 0, textproto.ProtocolError(fmt.Sprintf("Invalid binary: %s", line[8:]))
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *binaryReader) initData(size int) {
|
||||
if r.data == nil {
|
||||
r.data = make([]byte, size)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *binaryReader) readBytes(client *Client, size int) error {
|
||||
bytesError := client.readBytes(r.data[r.offset : r.offset+size])
|
||||
if bytesError != nil {
|
||||
return bytesError
|
||||
}
|
||||
|
||||
r.offset += size
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *binaryReader) updateOffsetArgument(args []string) []string {
|
||||
args[len(args)-1] = strconv.Itoa(r.offset)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (r *binaryReader) Binary() []byte {
|
||||
return r.data
|
||||
}
|
||||
189
internal/common/album.go
Normal file
189
internal/common/album.go
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"suruatoel.xyz/mcg/internal/i18n"
|
||||
)
|
||||
|
||||
type Album struct {
|
||||
title string
|
||||
songs []*Song
|
||||
artists []string
|
||||
pathes []string
|
||||
dates []string
|
||||
length uint64
|
||||
lastModified time.Time
|
||||
}
|
||||
|
||||
func NewAlbum(title string) *Album {
|
||||
album := Album{
|
||||
title: title,
|
||||
}
|
||||
|
||||
return &album
|
||||
}
|
||||
|
||||
func (a *Album) Title() string {
|
||||
return a.title
|
||||
}
|
||||
|
||||
func (a *Album) AddSong(song *Song) {
|
||||
a.songs = append(a.songs, song)
|
||||
a.length += song.Length()
|
||||
|
||||
for _, artist := range song.AlbumArtists() {
|
||||
if !slices.Contains(a.artists, artist) {
|
||||
a.artists = append(a.artists, artist)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(a.dates, song.Date()) {
|
||||
a.dates = append(a.dates, song.Date())
|
||||
}
|
||||
|
||||
path := filepath.Base(song.File())
|
||||
if !slices.Contains(a.pathes, path) {
|
||||
a.pathes = append(a.pathes, path)
|
||||
}
|
||||
|
||||
if !song.LastModified().After(a.lastModified) {
|
||||
a.lastModified = song.LastModified()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Album) Songs() []*Song {
|
||||
return a.songs
|
||||
}
|
||||
|
||||
func (a *Album) Artists() []string {
|
||||
return a.artists
|
||||
}
|
||||
|
||||
func (a *Album) Dates() []string {
|
||||
return a.dates
|
||||
}
|
||||
|
||||
func (a *Album) Length() uint64 {
|
||||
return a.length
|
||||
}
|
||||
|
||||
func (a *Album) LastModified() time.Time {
|
||||
return a.lastModified
|
||||
}
|
||||
|
||||
func (a *Album) ID() string {
|
||||
return generateAlbumId(a.title)
|
||||
}
|
||||
|
||||
func (a *Album) Tooltip() string {
|
||||
artists := strings.Join(a.Artists(), ", ")
|
||||
dates := strings.Join(a.dates, ", ")
|
||||
minutes := a.length / 60
|
||||
seconds := a.length - minutes*60
|
||||
length := fmt.Sprintf(i18n.T("%d:%d minutes"), minutes, seconds)
|
||||
|
||||
return strings.Join(
|
||||
[]string{
|
||||
a.title,
|
||||
dates,
|
||||
artists,
|
||||
length,
|
||||
},
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
|
||||
type PlaylistAlbum struct {
|
||||
*Album
|
||||
songs []*PlaylistSong
|
||||
length uint64
|
||||
}
|
||||
|
||||
func NewPlaylistAlbum(album *Album) *PlaylistAlbum {
|
||||
playlistAlbum := PlaylistAlbum{
|
||||
Album: album,
|
||||
}
|
||||
|
||||
return &playlistAlbum
|
||||
}
|
||||
|
||||
func (a *PlaylistAlbum) AddSong(playlistSong *PlaylistSong) {
|
||||
a.songs = append(a.songs, playlistSong)
|
||||
|
||||
song := playlistSong.Song
|
||||
a.length += song.Length()
|
||||
|
||||
for _, artist := range song.AlbumArtists() {
|
||||
if !slices.Contains(a.artists, artist) {
|
||||
a.artists = append(a.artists, artist)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(a.dates, song.Date()) {
|
||||
a.dates = append(a.dates, song.Date())
|
||||
}
|
||||
|
||||
path := filepath.Base(song.File())
|
||||
if !slices.Contains(a.pathes, path) {
|
||||
a.pathes = append(a.pathes, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *PlaylistAlbum) Songs() []*PlaylistSong {
|
||||
return a.songs
|
||||
}
|
||||
|
||||
func (a *PlaylistAlbum) Length() uint64 {
|
||||
return a.length
|
||||
}
|
||||
|
||||
func (a *Album) IsMatch(query string) bool {
|
||||
if query == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
for keyword := range strings.SplitSeq(query, " ") {
|
||||
if !a.isKeywordMatch(keyword) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Album) isKeywordMatch(keyword string) bool {
|
||||
if strings.Contains(strings.ToLower(a.title), keyword) {
|
||||
return true
|
||||
}
|
||||
for _, artist := range a.artists {
|
||||
if strings.Contains(strings.ToLower(artist), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, song := range a.songs {
|
||||
if strings.Contains(strings.ToLower(song.Title()), keyword) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(song.File()), keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func generateAlbumId(values ...string) string {
|
||||
h := md5.New()
|
||||
for _, value := range values {
|
||||
io.WriteString(h, value)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
14
internal/common/outputdevice.go
Normal file
14
internal/common/outputdevice.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package common
|
||||
|
||||
type OutputDevice struct {
|
||||
ID string
|
||||
Name string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
func NewOutputDevice(id string, name string) *OutputDevice {
|
||||
return &OutputDevice{
|
||||
ID: id,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
122
internal/common/song.go
Normal file
122
internal/common/song.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Song struct {
|
||||
artists []string
|
||||
title string
|
||||
file string
|
||||
albumartists []string
|
||||
track string
|
||||
length uint64
|
||||
date string
|
||||
lastModified time.Time
|
||||
}
|
||||
|
||||
func NewSong(artist string, title string, file string) *Song {
|
||||
song := Song{
|
||||
artists: []string{artist},
|
||||
title: title,
|
||||
file: file,
|
||||
}
|
||||
|
||||
return &song
|
||||
}
|
||||
|
||||
func (s *Song) AddArtist(artist string) {
|
||||
s.artists = append(s.artists, artist)
|
||||
}
|
||||
|
||||
func (s *Song) AddAlbumartist(artist string) {
|
||||
s.albumartists = append(s.albumartists, artist)
|
||||
}
|
||||
|
||||
func (s *Song) SongArtists() []string {
|
||||
if len(s.albumartists) > 0 {
|
||||
songArtists := []string{}
|
||||
for _, artist := range s.artists {
|
||||
if !slices.Contains(s.albumartists, artist) {
|
||||
songArtists = append(songArtists, artist)
|
||||
}
|
||||
}
|
||||
|
||||
return songArtists
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (s *Song) AlbumArtists() []string {
|
||||
if len(s.albumartists) > 0 {
|
||||
return s.albumartists
|
||||
}
|
||||
|
||||
return s.artists
|
||||
}
|
||||
|
||||
func (s *Song) Title() string {
|
||||
return s.title
|
||||
}
|
||||
|
||||
func (s *Song) File() string {
|
||||
return s.file
|
||||
}
|
||||
|
||||
func (s *Song) SetTrack(track string) {
|
||||
s.track = track
|
||||
}
|
||||
|
||||
func (s *Song) Track() string {
|
||||
return s.track
|
||||
}
|
||||
|
||||
func (s *Song) SetLength(length uint64) {
|
||||
s.length = length
|
||||
}
|
||||
|
||||
func (s *Song) Length() uint64 {
|
||||
return s.length
|
||||
}
|
||||
|
||||
func (s *Song) SetDate(date string) {
|
||||
s.date = date
|
||||
}
|
||||
|
||||
func (s *Song) Date() string {
|
||||
return s.date
|
||||
}
|
||||
|
||||
func (s *Song) SetLastModified(lastModified time.Time) {
|
||||
s.lastModified = lastModified
|
||||
}
|
||||
|
||||
func (s *Song) LastModified() time.Time {
|
||||
return s.lastModified
|
||||
}
|
||||
|
||||
type PlaylistSong struct {
|
||||
*Song
|
||||
id string
|
||||
pos uint64
|
||||
}
|
||||
|
||||
func NewPlaylistSong(song *Song, id string, pos uint64) *PlaylistSong {
|
||||
playlistSong := &PlaylistSong{
|
||||
Song: song,
|
||||
id: id,
|
||||
pos: pos,
|
||||
}
|
||||
|
||||
return playlistSong
|
||||
}
|
||||
|
||||
func (t *PlaylistSong) ID() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *PlaylistSong) Pos() uint64 {
|
||||
return t.pos
|
||||
}
|
||||
89
internal/common/sort.go
Normal file
89
internal/common/sort.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package common
|
||||
|
||||
import "sort"
|
||||
|
||||
type albumLessFunc func(p1, p2 *Album) bool
|
||||
|
||||
type albumSorter struct {
|
||||
albums []*Album
|
||||
less []albumLessFunc
|
||||
}
|
||||
|
||||
func (s *albumSorter) Len() int {
|
||||
return len(s.albums)
|
||||
}
|
||||
|
||||
func (s *albumSorter) Swap(i, j int) {
|
||||
s.albums[i], s.albums[j] = s.albums[j], s.albums[i]
|
||||
}
|
||||
|
||||
func (s *albumSorter) Less(i, j int) bool {
|
||||
p, q := s.albums[i], s.albums[j]
|
||||
|
||||
var k int
|
||||
for k = 0; k < len(s.less)-1; k++ {
|
||||
less := s.less[k]
|
||||
switch {
|
||||
case less(p, q):
|
||||
return true
|
||||
case less(q, p):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return s.less[k](p, q)
|
||||
}
|
||||
|
||||
func (s *albumSorter) Sort(albums []*Album) {
|
||||
s.albums = albums
|
||||
sort.Sort(s)
|
||||
}
|
||||
|
||||
func OrderAlbumBy(less ...albumLessFunc) *albumSorter {
|
||||
return &albumSorter{
|
||||
less: less,
|
||||
}
|
||||
}
|
||||
|
||||
func SortAlbums(album []*Album, field string) {
|
||||
title := orderAlbumByTitle
|
||||
artists := orderAlbumByArtists
|
||||
years := orderAlbumByYears
|
||||
modified := orderAlbumByModified
|
||||
|
||||
var sorter *albumSorter
|
||||
switch field {
|
||||
case "artist":
|
||||
sorter = OrderAlbumBy(artists, title, years, modified)
|
||||
case "year":
|
||||
sorter = OrderAlbumBy(years, title, artists, modified)
|
||||
case "modified":
|
||||
sorter = OrderAlbumBy(modified, title, artists, years)
|
||||
default:
|
||||
sorter = OrderAlbumBy(title, artists, years, modified)
|
||||
}
|
||||
|
||||
sorter.Sort(album)
|
||||
}
|
||||
|
||||
func orderAlbumByTitle(c1, c2 *Album) bool {
|
||||
return c1.Title() < c2.Title()
|
||||
}
|
||||
|
||||
func orderAlbumByArtists(left, right *Album) bool {
|
||||
if len(left.Artists()) > 0 && len(right.Artists()) > 0 {
|
||||
return left.Artists()[0] < right.Artists()[0]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func orderAlbumByYears(c1, c2 *Album) bool {
|
||||
if len(c1.Dates()) > 0 && len(c2.Dates()) > 0 {
|
||||
return c1.Dates()[0] < c2.Dates()[0]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func orderAlbumByModified(c1, c2 *Album) bool {
|
||||
return c1.LastModified().Before(c2.LastModified())
|
||||
}
|
||||
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.BuildMainCSS())
|
||||
|
||||
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(). | ||||