Compare commits

...

45 commits
v2.0.1 ... main

Author SHA1 Message Date
coderkun 75b99e5820 Port UI to GTK 4 (close #85) 2024-05-23 13:02:51 +02:00
coderkun 6ba8bc550f Bump version to 3.2.1 2023-02-28 13:18:07 +01:00
coderkun 21bd0f5832 Fix setting sort type on Library panel at startup (close #95) 2023-02-28 12:55:26 +01:00
coderkun bfb8eac62d Bump version to 3.2 2023-01-21 17:18:41 +01:00
coderkun 44fb332c62 Read cover with1 “readpicture” command if “albumart” is not found (close #90) 2023-01-21 15:37:29 +01:00
coderkun e976e05efe Fix setting sort order on Library panel (close #91) 2023-01-18 17:27:27 +01:00
coderkun a1e87e8994 Add new dependency “python-dateutil” to README 2023-01-18 17:27:27 +01:00
coderkun 8607bf15a9 Add option to sort by last modification date (close #88)
Store the metadate “last-modified” for a track and an album and add an
option sort by it.
2023-01-18 17:27:27 +01:00
coderkun 5618c9016c Always return tuple by get_albumart_now() (close #89) 2023-01-15 15:18:35 +01:00
Jeremy Fleischman ea71d96dd7
Don't assume that size file is valid
I don't know how my cache got into this state, but I had an empty size
file:

    $ cat ~/.cache/mcg/127.0.0.1/size

    $ stat ~/.cache/mcg/127.0.0.1/size
      File: /home/jeremy/.cache/mcg/127.0.0.1/size
      Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
    Device: 254,0	Inode: 18493061    Links: 1
    Access: (0644/-rw-r--r--)  Uid: ( 1000/  jeremy)   Gid: (  100/   users)
    Access: 2022-09-14 00:18:32.942885525 -0700
    Modify: 2022-09-07 12:32:44.151734944 -0700
    Change: 2022-09-07 12:32:44.151734944 -0700
     Birth: 2022-08-25 10:01:01.729717504 -0700

This was causing mcg's Library view to crash like this:

    (.mcg-wrapped:879276): Gtk-CRITICAL **: 00:19:15.727: gtk_window_add_accel_group: assertion 'GTK_IS_WINDOW (window)' failed
    Exception in thread Thread-1 (_set_playlist):
    Traceback (most recent call last):
      File "/nix/store/c1vb2z3c64i0sd92iz7fv0lb720qcvhb-python3-3.10.6/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
        self.run()
      File "/nix/store/c1vb2z3c64i0sd92iz7fv0lb720qcvhb-python3-3.10.6/lib/python3.10/threading.py", line 953, in run
        self._target(*self._args, **self._kwargs)
      File "/nix/store/l935dwmk93sq2chr4xxiipv9amyfcg43-CoverGrid-3.1/share/mcg/mcg/playlistpanel.py", line 256, in _set_playlist
        cache = client.MCGCache(host, size)
      File "/nix/store/l935dwmk93sq2chr4xxiipv9amyfcg43-CoverGrid-3.1/share/mcg/mcg/client.py", line 1279, in __init__
        self._read_size()
      File "/nix/store/l935dwmk93sq2chr4xxiipv9amyfcg43-CoverGrid-3.1/share/mcg/mcg/client.py", line 1293, in _read_size
        size = int(f.readline())
    ValueError: invalid literal for int() with base 10: ''

Maybe mcg crashed while writing the `size` file at some point? I see
that it writes directly to the size file, which seems potentially risky:
it would probably be safer to write to a temp file and then (atomically)
move it. Still, it seems like a good practice to be resilient here.

After this change, here's what I see get printed by mcg:

    (.mcg-wrapped:889856): Gtk-CRITICAL **: 00:37:00.045: gtk_window_add_accel_group: assertion 'GTK_IS_WINDOW (window)' failed
    2022-09-14 00:37:00,076 WARNING: invalid cache file: /home/jeremy/.cache/mcg/127.0.0.1/size, deleting file
    Traceback (most recent call last):
      File "/nix/store/vzgcfs00nq543hjk8hrk81k1rs8aqpqw-CoverGrid-3.1/share/mcg/mcg/client.py", line 1295, in _read_size
        size = int(f.readline())
    ValueError: invalid literal for int() with base 10: ''

And then the problem goes away =)
2022-09-14 00:37:52 -07:00
coderkun 48b1d90fc8 Bump version to 3.1 2022-09-11 18:13:52 +02:00
coderkun 37b8701361 Use Markdown for README file (close #86) 2022-09-11 12:48:55 +02:00
coderkun fac7a85566 Use the build system “meson” (close #32)
Replace the build system “setuptools” with “meson”.
2022-09-11 12:31:13 +02:00
coderkun ff0eee8380 Bump version to 3.0.2 2022-09-11 12:28:24 +02:00
coderkun e63a3f2d4d Fix volume button (close #84)
Fix both the icon of the volume button and setting the volume if the
mixer does not allow it by using -1 as default value instead of 0.
2022-09-11 12:24:56 +02:00
Jeremy Fleischman 54717a3491 Fix build
When doing a `python setup.py build` on my machine, I found that
`build/lib` would not end up with a compiled gresource file until the
second invocation of `python setup.py build`.

Before:

    $ python setup.py build
    running build
    running build_py
    creating build
    creating build/lib
    creating build/lib/mcg
    copying mcg/connectionpanel.py -> build/lib/mcg
    copying mcg/shortcutsdialog.py -> build/lib/mcg
    copying mcg/serverpanel.py -> build/lib/mcg
    copying mcg/application.py -> build/lib/mcg
    copying mcg/window.py -> build/lib/mcg
    copying mcg/playlistpanel.py -> build/lib/mcg
    copying mcg/utils.py -> build/lib/mcg
    copying mcg/coverpanel.py -> build/lib/mcg
    copying mcg/infodialog.py -> build/lib/mcg
    copying mcg/librarypanel.py -> build/lib/mcg
    copying mcg/client.py -> build/lib/mcg
    copying mcg/zeroconf.py -> build/lib/mcg
    copying mcg/mcg.py -> build/lib/mcg
    copying mcg/__init__.py -> build/lib/mcg
    copying mcg/albumheaderbar.py -> build/lib/mcg
    package init file 'data/__init__.py' not found (or not a regular file)
    compiling gresources
    compiling gschemas

    $ ls build/lib/mcg/data/
    ls: cannot access 'build/lib/mcg/data/': No such file or directory

Note how there is no data directory at all. Now check out what happens
on the second build:

    $ git status
    On branch main
    Your branch is up to date with 'origin/main'.

    Untracked files:
      (use "git add <file>..." to include in what will be committed)
            data/gschemas.compiled
            data/xyz.suruatoel.mcg.gresource

    nothing added to commit but untracked files present (use "git add" to track)

    $ python setup.py build
    running build
    running build_py
    package init file 'data/__init__.py' not found (or not a regular file)
    creating build/lib/mcg/data
    copying data/xyz.suruatoel.mcg.gresource -> build/lib/mcg/data
    compiling gresources
    compiling gschemas

    $ ls build/lib/mcg/data/
    xyz.suruatoel.mcg.gresource

That's because the first build generated the compiled schemas and
resources (you can see evidence of that in `git status`), and then the
second build was able to copy the gresource file over according to the
`package_data` rules. The fix I've introduced here is to just do the
compilations *before* we call `super(...).run(...)`. There might be
better ways of doing this, I'm not very familiar with packaging gtk
python applications.

Things were even worse for the gschemas.compiled file: in addition to
the ordering issue it's not even mentioned in `data_files`, so even if
it does exist, it doesn't have a chance to get copied over when
installed. So I've added it to the `data_files` section. I don't know if
that'll play nicely or not with the existing `--no-compile-schemas`
flag.
2022-09-11 12:12:20 +02:00
coderkun 92737cd045 Update requirements in README
Add the minimum required version of GTK and MPD to the requirements in
the README.
2021-04-18 18:10:05 +02:00
coderkun ac14c8c7c7 Bump version to 3.0.1 2021-04-18 18:04:34 +02:00
coderkun 16030a2053 Fix icon path in setup.py 2021-04-18 18:03:55 +02:00
coderkun b90ce3299f Bump version to 3.0 2021-04-18 17:14:04 +02:00
coderkun 04effa0ec1 Fix error handling to operate on error number
Fix the handling of MPD errors to compare the error number instead of
the complete, unparsed error message.
2021-04-18 17:09:35 +02:00
coderkun f843cc629d Set logo for info dialog via code
Set the logo for the info dialog via code instead of UI file to fix
sizing issue.
2021-04-18 16:50:43 +02:00
coderkun faec824e8b Upate GTK UI files
Upate all GTK UI files by saving them with Glade 3.38.
2021-04-17 13:40:47 +02:00
coderkun 0a631877df Fix setting albumart on UI widgets 2020-10-24 14:58:28 +02:00
coderkun 8714d7a309 Update translation catalogues 2020-10-24 14:43:00 +02:00
coderkun 83082c3265 Add back shortcut to search the library 2020-10-24 14:37:38 +02:00
coderkun 32d02f2d9b Update GTK resources and schema to new domain (close #66) 2020-08-09 11:09:53 +02:00
coderkun ba373ddf4e Use GTK Composite Templates (close #62)
Use GTK Composite Templates for GUI elements to clean up and simplify
the code for widgets and all UI elements. This includes splitting the
large “gtk.glade” file into smaller .ui files and the large “widgets.py”
file into smaller .py files.
2020-08-09 10:57:01 +02:00
coderkun f4b545369c Merge release v2.1.2 2020-08-03 16:29:17 +02:00
coderkun 17fe4ee8ca Bump version to 2.1.2 2020-08-03 16:27:31 +02:00
coderkun 973d3dd921 Load playlist before status (close #72)
Load the playlist before loading the status for the idle event “changed”
to make sure the playlist information is attached to the current album
correctly.
2020-08-02 17:39:04 +02:00
coderkun bb8b816e8f Support “albumart” command (close #30)
Load the album covers using MPD’s new “albumart” command instead of
reading the covers from the harddrive. Remove the corresponding UI
elements and configuration option.
2020-07-26 09:40:33 +02:00
coderkun c53681ea82 Use custom buffer for reading from socket
Use the recv() method to read data from the socket instead of makefile()
to allow reading of binary data that is not text. This requires using a
custom buffer.
2020-07-26 09:40:33 +02:00
coderkun 83990c8796 Bump version to 2.1.1 2020-07-26 09:26:57 +02:00
coderkun ddf8368bfd Fix shortcut to exit fullscreen mode (close #71)
Fix the shortcut for fullscreen mode to also exit it. Additionally fix
the shortcuts window to show the correct shortcut for fullscreen mode.
2020-07-25 14:25:29 +02:00
coderkun 9ad3086ace Bump version to 2.1 2020-03-22 15:43:28 +01:00
coderkun 4708344d4d Fix localization in Utils class
Fix the localization in the Utils class by using the “gettext” instead
of the “locale” module.
2020-03-22 15:39:58 +01:00
coderkun 704fa3278a Show album length on Playlist and Library panel (close #57)
Show the length of an album in the tooltip on the Playlist and the
Library panel.
2020-03-22 11:19:03 +01:00
coderkun 477f89cc0b Init progress bar on Library panel (close #49)
Introduce two new callbacks for this: one when initializing the loading
of albums and another one on handling the loading of each album. Use the
first one to initialize and the second one to pulse the progress bar on
the Library panel.

Additionally use the text of the progress as status label instead of a
separate label widget.
2020-03-22 11:14:45 +01:00
coderkun 14d56452b6 Set max width for tracks on Cover panel (close #64) 2020-03-22 11:10:41 +01:00
coderkun 8ecc7176d7 Configure icon for application window (close #61) 2020-03-21 22:39:20 +01:00
coderkun 21f37dc62c Bump version to 2.0.2 2020-03-15 15:23:08 +01:00
coderkun aa196aa994 Save file “gtk.glade” with Glade 3.22.2 2020-03-15 15:22:40 +01:00
coderkun 6801dc9edd Fix setting default image
Fix setting the default image (if the selected/current album does not
has a cover) for the Cover panel, the Playlist panel and the Library
panel by using the default image instead of clearing the image widget.
Additionally fix the check for the empty URL String.
2020-03-15 15:19:04 +01:00
coderkun c8e24dc8c1 Update screenshots in readme 2019-03-23 18:14:36 +00:00
56 changed files with 4979 additions and 5550 deletions

6
.gitignore vendored
View file

@ -3,7 +3,5 @@ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# Files created by setuptools /build/
build/ /install/
dist/
*.egg-info/

63
README.md Normal file
View file

@ -0,0 +1,63 @@
# CoverGrid
CoverGrid (mcg) is a client for the [Music Player
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
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)
Additionally a [MPD](http://www.musicpd.org) server (version >= 0.21.0) is
required at runtime.
## Building
Build the application with _meson_ and _ninja_:
$ meson build
$ ninja -C build
## Running/Testing
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`:
$ meson --prefix $(pwd)/install build
$ ninja -C build
$ ninja -C build install
After that you can run it with _mesons_ `devenv` command:
$ meson devenv -C build src/mcg
## Installing
Install the application system-wide with _meson_ (after building):
$ ninja -C build install
Note: _On Linux using the distributions package manager is the preferred way
of installing applications system-wide._
## Screenshots
![CoverGrids cover panel with album details and track list.](https://suruatoel.xyz/images/mcg-cover-s.png "CoverGrids cover panel with album details and track list.")
![CoverGrids playlist panel with queued albums.](https://suruatoel.xyz/images/mcg-playlist.png "CoverGrids playlist panel with queued albums.")
![CoverGrids library panel showing the albums middle-sized.](https://suruatoel.xyz/images/mcg-library-m.png "CoverGrids library panel showing the albums middle-sized.")
![CoverGrids library panel showing the albums small-sized.](https://suruatoel.xyz/images/mcg-library-s.png "CoverGrids library panel showing the albums small-sized.")

View file

@ -1,80 +0,0 @@
h1. CoverGrid
CoverGrid (mcg) is a client for the "Music Player 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
License: "GPL":http://www.gnu.org/licenses/gpl.html v3
Dependencies:
* "Python":http://www.python.org 3
* "GTK":http://www.gtk.org 3 ("python-gobject":https://live.gnome.org/PyGObject)
* "Avahi":http://www.avahi.org (optional)
* "python-keyring":http://pypi.python.org/pypi/keyring (optional)
* "python-setuptools":https://pypi.python.org/pypi/setuptools (building)
h2. Building
Build the application with _setuptools_:
bc. $ python3 setup.py build
h2. Running/Testing
For testing the application and running it without (system-wide) installation, donwload/clone the code, build it as described above and then use _setuptools_ to install it for the current user:
bc. $ python3 setup.py develop --user
After that you can run it with
bc. $ ~/.local/bin/mcg
or if _~/.local/bin/_ is on your PATH
bc. $ mcg
h2. Installing
Install the application system-wide with _setuptools_:
bc. # python3 setup.py install
Note: _On Linux using the distributions package manager is the preferred way of installing applications system-wide._
h2. Packaging
Create a distribution package with _setuptools_:
bc. $ python3 setup.py sdist
h2. Cover/image configuration
Since MPD itself does not provide the cover/image/album art binaries, yet, _mcg_ has to look for them itself. In order to find the images a base folder has to be configured on the Connection tab as “Image Directory”. This value can either be a local (absolute) folder (e.g. /home/user/music/) or an http URL (e.g. http://localhost/music/). _mcg_ then adds the (relative) folder of the audio file to this paths and tries different names for the actual file:
# the album name
# “cover”
# “folder”
The following file extensions are used:
# png
# jpg
The first combination that results in an existing file is used as cover image. If no matching file exists, _mcg_ will try to traverse the directory for any image file as fallback—this is done for local paths only though, not for http URLs.
h2. Screenshots
!https://cloud.suruatoel.xyz/s/kx6oyfcXBaytmkD/preview(Cover)!
!https://cloud.suruatoel.xyz/s/at84Z9dnbRycWZS/preview(Playlist)!
!https://cloud.suruatoel.xyz/s/AEBZqsJ5E6SKTY2/preview(Library (middle-sized))!
!https://cloud.suruatoel.xyz/s/27a28kDSQQ5JTET/preview(Library (small-sized))!

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/de/coderkun/mcg">
<file>gtk.glade</file>
<file>gtk.css</file>
<file>gtk.shortcuts.ui</file>
<file>gtk.menu.ui</file>
<file>mcg.svg</file>
<file>noise-texture.png</file>
</gresource>
</gresources>

View file

@ -1,8 +1,20 @@
.bg-texture { #content_stack {
box-shadow:inset 4px 4px 10px rgba(0,0,0,0.3); box-shadow:inset 4px 4px 10px rgba(0,0,0,0.3);
background-image:url('noise-texture.png'); background-image:url('noise-texture.png');
} }
#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 { .no-bg {
background:none; background:none;
} }
@ -18,13 +30,17 @@
font-weight:bold; font-weight:bold;
} }
revealer.sidebar > * { window.fullscreen #cover_box {
background-color:alpha(@theme_bg_color, 0.8); background: black;
box-shadow:0 0 10px @theme_bg_color;
margin-left:20px
} }
revealer.sidebar scale mark indicator { #cover_info_revealer {
background-color:alpha(@theme_bg_color, 0.8);
box-shadow:0 0 10px @theme_bg_color;
margin-left:20px;
}
#cover_info_revealer scale mark indicator {
margin-right:5px; margin-right:5px;
} }
@ -32,24 +48,13 @@ actionbar {
background-color:@theme_unfocused_bg_color; background-color:@theme_unfocused_bg_color;
} }
/* Icon View in regular mode */ gridview child {
iconview.view:selected, padding: 1px;
iconview.view:selected:focus {
background-color:@theme_selected_bg_color;
} }
iconview.view:hover { gridview.selection child {
-gtk-icon-effect:highlight; opacity: 0.5;
} }
gridview.selection child:hover,
/* Icon View in selection mode */ gridview.selection child:selected {
iconview.view.selection { opacity: 1;
-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;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,65 +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">&lt;Primary&gt;c</attribute>
</item>
<item>
<attribute name="action">win.play</attribute>
<attribute name="label" translatable="yes">Play</attribute>
<attribute name="accel">&lt;Primary&gt;p</attribute>
</item>
<item>
<attribute name="action">win.clear-playlist</attribute>
<attribute name="label" translatable="yes">Clear Playlist</attribute>
<attribute name="accel">&lt;Primary&gt;r</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">&lt;Primary&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;KP_4</attribute>
</item>
</section>
<section>
<item>
<attribute name="action">app.shortcuts</attribute>
<attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
<attribute name="accel">&lt;Primary&gt;k</attribute>
</item>
<item>
<attribute name="action">app.info</attribute>
<attribute name="label" translatable="yes">Info</attribute>
<attribute name="accel">&lt;Primary&gt;i</attribute>
</item>
<item>
<attribute name="action">app.quit</attribute>
<attribute name="label" translatable="yes">Quit</attribute>
<attribute name="accel">&lt;Primary&gt;q</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
@ -9,81 +7,82 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024" sodipodi:docname="mcg.svg"
height="1024" inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
viewBox="0 0 270.93333 270.93334"
version="1.1"
id="svg8" id="svg8"
inkscape:version="0.92.1 r" version="1.1"
sodipodi:docname="mcg.svg"> viewBox="0 0 67.733333 67.733335"
height="256"
width="256">
<title <title
id="title4619">CoverGrid (mcg)</title> id="title4619">CoverGrid (mcg)</title>
<defs <defs
id="defs2"> id="defs2">
<filter <filter
style="color-interpolation-filters:sRGB;" id="filter4516"
inkscape:label="Drop Shadow" inkscape:label="Drop Shadow"
id="filter4516"> style="color-interpolation-filters:sRGB">
<feFlood <feFlood
flood-opacity="0.498039" id="feFlood4506"
flood-color="rgb(255,255,255)"
result="flood" result="flood"
id="feFlood4506" /> flood-color="rgb(255,255,255)"
flood-opacity="0.498039" />
<feComposite <feComposite
in="flood" id="feComposite4508"
in2="SourceGraphic"
operator="in"
result="composite1" result="composite1"
id="feComposite4508" /> operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur <feGaussianBlur
in="composite1" id="feGaussianBlur4510"
stdDeviation="7"
result="blur" result="blur"
id="feGaussianBlur4510" /> stdDeviation="7"
in="composite1" />
<feOffset <feOffset
dx="6" id="feOffset4512"
dy="6"
result="offset" result="offset"
id="feOffset4512" /> dy="6"
dx="6" />
<feComposite <feComposite
in="SourceGraphic" id="feComposite4514"
in2="offset"
operator="over"
result="composite2" result="composite2"
id="feComposite4514" /> operator="over"
in2="offset"
in="SourceGraphic" />
</filter> </filter>
</defs> </defs>
<sodipodi:namedview <sodipodi:namedview
id="base" inkscape:document-rotation="0"
pagecolor="#ffffff" inkscape:snap-to-guides="true"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="107.73101"
inkscape:cy="490.36801"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
units="px"
inkscape:pagecheckerboard="true"
showborder="true"
inkscape:showpageshadow="false"
inkscape:snap-grids="true" inkscape:snap-grids="true"
inkscape:snap-to-guides="true"> inkscape:showpageshadow="false"
showborder="true"
inkscape:pagecheckerboard="true"
units="px"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:window-height="1030"
inkscape:window-width="1916"
inkscape:guide-bbox="true"
showguides="true"
showgrid="false"
inkscape:current-layer="layer1"
inkscape:document-units="mm"
inkscape:cy="490.36801"
inkscape:cx="311.30244"
inkscape:zoom="0.7"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base">
<sodipodi:guide <sodipodi:guide
position="319.76785,-21.166667" inkscape:locked="false"
orientation="0,1"
id="guide4676" id="guide4676"
inkscape:locked="false" /> orientation="0,1"
position="319.76785,-21.166667" />
</sodipodi:namedview> </sodipodi:namedview>
<metadata <metadata
id="metadata5"> id="metadata5">
@ -120,94 +119,95 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g <g
inkscape:label="Ebene 1" transform="translate(0,-26.06665)"
inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(0,-26.06665)"> inkscape:groupmode="layer"
inkscape:label="Ebene 1">
<g <g
id="g4504" transform="matrix(0.253073,0,0,0.25346533,-0.4687123,18.990219)"
transform="matrix(0.8231966,0,0,0.8231966,23.950969,28.559636)"> id="g4504">
<rect <rect
y="117.61248" style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="91.54583"
height="87.841667"
width="87.841667"
id="rect4487" id="rect4487"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="27.918732"
x="91.54583"
height="87.841667"
width="87.841667" width="87.841667"
height="87.841667"
x="91.54583"
y="117.61248" />
<rect
style="fill:#5f3262;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-1" id="rect4487-1"
style="fill:#5f3262;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
y="-115.7604"
x="181.23958"
height="87.841667"
width="87.841667" width="87.841667"
id="rect4487-8"
style="fill:#9ba38f;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="207.30624"
x="181.23958"
height="87.841667" height="87.841667"
width="87.841667"
id="rect4487-19"
style="fill:#9aa0af;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="117.61248"
x="181.23958"
height="87.841667"
width="87.841667"
id="rect4487-7"
style="fill:#e62a7c;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="27.918732"
x="1.8520833"
height="87.841667"
width="87.841667"
id="rect4487-2"
style="fill:#c5d0f2;fill-opacity:1;stroke:none;stroke-width:1.05833328;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="207.30623"
x="91.54583" x="91.54583"
height="87.841667" y="27.918732" />
<rect
style="fill:#9ba38f;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-8"
width="87.841667" width="87.841667"
height="87.841667"
x="181.23958"
y="-115.7604"
transform="scale(1,-1)" />
<rect
style="fill:#9aa0af;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-19"
width="87.841667"
height="87.841667"
x="181.23958"
y="207.30624" />
<rect
style="fill:#e62a7c;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-7"
width="87.841667"
height="87.841667"
x="181.23958"
y="117.61248" />
<rect
style="fill:#c5d0f2;fill-opacity:1;stroke:none;stroke-width:1.05833;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-2"
width="87.841667"
height="87.841667"
x="1.8520833"
y="27.918732" />
<rect
style="fill:#9a0c98;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-23" id="rect4487-23"
style="fill:#9a0c98;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="207.30623"
x="1.8520833"
height="87.841667"
width="87.841667" width="87.841667"
height="87.841667"
x="91.54583"
y="207.30623" />
<rect
style="fill:#e4cec8;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-66" id="rect4487-66"
style="fill:#e4cec8;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
y="117.61248"
x="1.8520833"
height="87.841667"
width="87.841667" width="87.841667"
height="87.841667"
x="1.8520833"
y="207.30623" />
<rect
style="fill:#fd6bfc;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487-4" id="rect4487-4"
style="fill:#fd6bfc;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> width="87.841667"
height="87.841667"
x="1.8520833"
y="117.61248" />
</g> </g>
<path <path
sodipodi:type="star" transform="matrix(0.30742717,0,0,0.30790377,0,18.04063)"
style="opacity:0.57999998;fill:#000000;fill-opacity:1;stroke-width:0.98095167;filter:url(#filter4516)" inkscape:transform-center-y="-0.54802213"
id="path4661" inkscape:transform-center-x="5.196062"
sodipodi:sides="3" d="M 204.82033,136.72273 50.691586,224.82232 51.459481,47.293117 Z"
sodipodi:cx="127.79939"
sodipodi:cy="161.75498"
sodipodi:r1="102.49749"
sodipodi:r2="97.404572"
sodipodi:arg1="0.0043254268"
sodipodi:arg2="1.051523"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0" inkscape:randomized="0"
d="M 230.29592,162.19832 76.16718,250.2979 76.935074,72.768703 Z" inkscape:rounded="0"
inkscape:transform-center-x="16.901764" inkscape:flatsided="true"
inkscape:transform-center-y="-1.779849" /> sodipodi:arg2="1.051523"
sodipodi:arg1="0.0043254268"
sodipodi:r2="97.404572"
sodipodi:r1="102.49749"
sodipodi:cy="136.27939"
sodipodi:cx="102.3238"
sodipodi:sides="3"
id="path4661"
style="opacity:0.58;fill:#000000;fill-opacity:1;stroke-width:0.980952;filter:url(#filter4516)"
sodipodi:type="star" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

7
data/icons/meson.build Normal file
View file

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

View file

@ -4,7 +4,7 @@ Name=CoverGrid (mcg)
Comment=CoverGrid for the Music Player Daemon Comment=CoverGrid for the Music Player Daemon
Keywords=mpd; Keywords=mpd;
Type=Application Type=Application
Icon=mcg.svg Icon=xyz.suruatoel.mcg
Exec=mcg Exec=mcg
Categories=AudioVideo; Categories=AudioVideo;
StartupNotify=true StartupNotify=true

BIN
data/mcg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

34
data/meson.build Normal file
View file

@ -0,0 +1,34 @@
i18n = import('i18n')
desktop_file = i18n.merge_file(
input: 'mcg.desktop',
output: 'mcg.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'applications')
)
desktop_utils = find_program('desktop-file-validate', required: false)
if desktop_utils.found()
test('Validate desktop file', desktop_utils,
args: [desktop_file]
)
endif
gnome = import('gnome')
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
gnome.compile_resources('mcg',
'xyz.suruatoel.mcg.gresource.xml',
gresource_bundle: true,
install: true,
install_dir: pkgdatadir,
)
install_data('xyz.suruatoel.mcg.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
gnome.compile_schemas(depend_files: files('xyz.suruatoel.mcg.gschema.xml'))
subdir('icons')

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="4.8"/>
<requires lib="adw" version="1.2" />
<template class="McgAlbumHeaderbar" parent="AdwBin">
<child>
<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="GtkButton">
<property name="receives-default">True</property>
<signal name="clicked" handler="on_close_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<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>
<property name="value">6600</property>
<property name="step-increment">1</property>
<property name="page-increment">100</property>
</object>
<object class="GtkBox" id="toolbar">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
</object>
<template class="McgConnectionPanel" parent="AdwBin">
<child>
<object class="AdwStatusPage">
<property name="title">Connect to MPD</property>
<child>
<object class="GtkBox">
<property name="hexpand">false</property>
<property name="halign">center</property>
<child>
<object class="GtkListBox" id="zeroconf_list">
<property name="hexpand">true</property>
<child type="placeholder">
<object class="GtkLabel">
<property name="label" translatable="yes">No service found</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<property name="hexpand">true</property>
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
<child>
<object class="AdwEntryRow" id="host_row">
<property name="title" translatable="yes">Host</property>
<property name="show-apply-button">true</property>
<signal name="apply" handler="on_host_entry_apply"/>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<style>
<class name="header"/>
</style>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Port</property>
<property name="halign">start</property>
<property name="hexpand">false</property>
<style>
<class name="subtitle"/>
</style>
</object>
</child>
<child>
<object class="GtkSpinButton" id="port_spinner">
<property name="name">port_spinner</property>
<property name="value">6600</property>
<property name="adjustment">server-port-adjustment</property>
<signal name="value-changed" handler="on_port_spinner_value_changed"/>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="password_row">
<property name="title" translatable="yes">Password</property>
<property name="show-apply-button">true</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

161
data/ui/cover-panel.ui Normal file
View file

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<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>
<template class="McgCoverPanel" parent="GtkOverlay">
<child>
<object class="GtkStack" id="cover_stack">
<property name="visible-child">cover_default</property>
<child>
<object class="GtkSpinner" id="cover_spinner">
</object>
</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="kinetic-scrolling">False</property>
<property name="overlay-scrolling">False</property>
<property name="hexpand">true</property>
<property name="halign">fill</property>
<property name="vexpand">true</property>
<property name="valign">fill</property>
<child>
<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="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>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkRevealer" id="info_revealer">
<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="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">
<child>
<object class="GtkBox">
<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>
<child>
<object class="GtkGrid">
<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="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>
</child>
<child>
<object class="GtkLabel" id="album_date_label">
<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>
</child>
<child>
<object class="GtkLabel" id="album_artist_label">
<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>
</child>
</object>
</child>
<child>
<object class="GtkScale" id="songs_scale">
<property name="orientation">vertical</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>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

340
data/ui/library-panel.ui Normal file
View file

@ -0,0 +1,340 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<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>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="halign">end</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="value-changed" handler="on_grid_scale_changed" swapped="no"/>
</object>
</child>
<child>
<object class="GtkButton" id="library-toolbar-update">
<property name="label" translatable="yes">update library</property>
<signal name="clicked" handler="on_update_clicked" swapped="no"/>
</object>
</child>
<child>
<object class="GtkBox" id="library-toolbar-sort">
<property name="orientation">vertical</property>
<child>
<object class="GtkSeparator">
<property name="orientation">vertical</property>
</object>
</child>
<child>
<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>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</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>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</object>
</child>
<child>
<object class="GtkCheckButton" id="sort_year">
<property name="label" translatable="yes">sort by year</property>
<property name="receives-default">False</property>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</object>
</child>
<child>
<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>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</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>
<property name="receives-default">False</property>
<property name="active">True</property>
<signal name="toggled" handler="on_sort_order_toggled" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
</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>
<!--
<accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/>
-->
</object>
</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="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="icon-name">open-menu-symbolic</property>
</object>
</child>
</object>
</child>
</object>
<template class="McgLibraryPanel" parent="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="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>
<signal name="search-changed" handler="on_filter_entry_changed" swapped="no"/>
</object>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox" id="progress_box">
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<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>
<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>
<signal name="activate" handler="on_library_grid_clicked"/>
<style>
<class name="no-bg"/>
</style>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes">
<![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="activatable">true</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkPicture">
<property name="content-fit">contain</property>
<property name="can-shrink">false</property>
<binding name="tooltip-markup">
<lookup name="tooltip" type="GridItem">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<binding name="paintable">
<lookup name="cover" type="GridItem">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
</object>
</property>
</template>
</interface>
]]>
</property>
</object>
</property>
</object>
</child>
<style>
<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">
<property name="label" translatable="yes">cancel</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
</object>
</child>
<child type="end">
<object class="GtkButton">
<property name="label" translatable="yes">queue</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_add_clicked" swapped="no"/>
</object>
</child>
</object>
</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">
<property name="label" translatable="yes">play</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
</object>
</child>
<child type="end">
<object class="GtkButton">
<property name="label" translatable="yes">queue</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_queue_clicked" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

182
data/ui/playlist-panel.ui Normal file
View file

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<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="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="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>
<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>
</child>
</object>
<template class="McgPlaylistPanel" parent="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="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>
<signal name="activate" handler="on_playlist_grid_clicked"/>
<style>
<class name="no-bg"/>
</style>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes">
<![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="activatable">true</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkPicture">
<property name="content-fit">contain</property>
<property name="can-shrink">false</property>
<binding name="tooltip-markup">
<lookup name="tooltip" type="GridItem">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<binding name="paintable">
<lookup name="cover" type="GridItem">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
</object>
</property>
</template>
</interface>
]]>
</property>
</object>
</property>
</object>
</child>
<style>
<class name="no-bg"/>
</style>
</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">
<property name="label" translatable="yes">cancel</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
</object>
</child>
<child type="end">
<object class="GtkButton">
<property name="label" translatable="yes">remove</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_remove_clicked" swapped="no"/>
</object>
</child>
</object>
</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>
<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">
<property name="label" translatable="yes">play</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
</object>
</child>
<child type="end">
<object class="GtkButton">
<property name="label" translatable="yes">remove</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_remove_clicked" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

326
data/ui/server-panel.ui Normal file
View file

@ -0,0 +1,326 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<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="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>
</child>
</object>
<template class="McgServerPanel" parent="AdwBin">
<child>
<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="GtkGrid">
<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="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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel" id="status_file">
<property name="halign">start</property>
<property name="label" translatable="yes">&lt;i&gt;none&lt;/i&gt;</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">&lt;i&gt;none&lt;/i&gt;</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">&lt;i&gt;none&lt;/i&gt;</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>
</child>
<child>
<object class="GtkLabel" id="status_error">
<property name="halign">start</property>
<property name="label" translatable="yes">&lt;i&gt;none&lt;/i&gt;</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>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<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="GtkGrid">
<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="halign">end</property>
<property name="justify">right</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>
</child>
<child>
<object class="GtkLabel" id="stats_albums">
<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>
</child>
<child>
<object class="GtkLabel" id="stats_songs">
<property name="halign">end</property>
<property name="justify">right</property>
<layout>
<property name="column">0</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel" id="stats_dbplaytime">
<property name="halign">end</property>
<property name="justify">right</property>
<layout>
<property name="column">0</property>
<property name="row">3</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel">
<layout>
<property name="column">0</property>
<property name="row">4</property>
<property name="column-span">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel" id="stats_playtime">
<property name="halign">end</property>
<property name="justify">right</property>
<layout>
<property name="column">0</property>
<property name="row">5</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<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="hexpand">false</property>
<property name="halign">center</property>
<property name="selection-mode">none</property>
<style>
<class name="no-bg"/>
</style>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</property>
</object>
</child>
</template>
</interface>

View file

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<interface> <interface>
<requires lib="gtk+" version="3.18"/> <requires lib="gtk+" version="4.8"/>
<object class="GtkShortcutsWindow" id="shortcuts-dialog"> <requires lib="adw" version="1.2" />
<property name="modal">True</property> <template class="McgShortcutsDialog" parent="GtkShortcutsWindow">
<property name="modal">1</property>
<child> <child>
<object class="GtkShortcutsSection"> <object class="GtkShortcutsSection">
<property name="visible">1</property> <property name="visible">1</property>
@ -10,53 +11,45 @@
<property name="max-height">10</property> <property name="max-height">10</property>
<child> <child>
<object class="GtkShortcutsGroup"> <object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">General</property> <property name="title" translatable="yes">General</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_1</property> <property name="accelerator">&lt;primary&gt;KP_1</property>
<property name="title" translatable="yes">Switch to the Connection panel</property> <property name="title" translatable="yes">Switch to the Connection panel</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_2</property> <property name="accelerator">&lt;primary&gt;KP_2</property>
<property name="title" translatable="yes">Switch to the Cover panel</property> <property name="title" translatable="yes">Switch to the Cover panel</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_3</property> <property name="accelerator">&lt;primary&gt;KP_3</property>
<property name="title" translatable="yes">Switch to the Playlist panel</property> <property name="title" translatable="yes">Switch to the Playlist panel</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_4</property> <property name="accelerator">&lt;primary&gt;KP_4</property>
<property name="title" translatable="yes">Switch to the Library panel</property> <property name="title" translatable="yes">Switch to the Library panel</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;k</property> <property name="accelerator">&lt;primary&gt;k</property>
<property name="title" translatable="yes">Show the keyboard shortcuts</property> <property name="title" translatable="yes">Show the keyboard shortcuts</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;i</property> <property name="accelerator">&lt;primary&gt;i</property>
<property name="title" translatable="yes">Open the info dialog</property> <property name="title" translatable="yes">Open the info dialog</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;q</property> <property name="accelerator">&lt;primary&gt;q</property>
<property name="title" translatable="yes">Quit the application</property> <property name="title" translatable="yes">Quit the application</property>
</object> </object>
@ -65,25 +58,21 @@
</child> </child>
<child> <child>
<object class="GtkShortcutsGroup"> <object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">Player</property> <property name="title" translatable="yes">Player</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;c</property> <property name="accelerator">&lt;primary&gt;c</property>
<property name="title" translatable="yes">Connect or disconnect</property> <property name="title" translatable="yes">Connect or disconnect</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;p</property> <property name="accelerator">&lt;primary&gt;p</property>
<property name="title" translatable="yes">Switch between play and pause</property> <property name="title" translatable="yes">Switch between play and pause</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;r</property> <property name="accelerator">&lt;primary&gt;r</property>
<property name="title" translatable="yes">Clear the playlist</property> <property name="title" translatable="yes">Clear the playlist</property>
</object> </object>
@ -92,12 +81,10 @@
</child> </child>
<child> <child>
<object class="GtkShortcutsGroup"> <object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">Cover Panel</property> <property name="title" translatable="yes">Cover Panel</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="accelerator">F11</property>
<property name="accelerator">&lt;alt&gt;Return F11</property>
<property name="title" translatable="yes">Show the cover in fullscreen mode</property> <property name="title" translatable="yes">Show the cover in fullscreen mode</property>
</object> </object>
</child> </child>
@ -105,11 +92,9 @@
</child> </child>
<child> <child>
<object class="GtkShortcutsGroup"> <object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">Library Panel</property> <property name="title" translatable="yes">Library Panel</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;f</property> <property name="accelerator">&lt;primary&gt;f</property>
<property name="title" translatable="yes">Search the library</property> <property name="title" translatable="yes">Search the library</property>
</object> </object>
@ -118,5 +103,5 @@
</child> </child>
</object> </object>
</child> </child>
</object> </template>
</interface> </interface>

78
data/ui/window.ui Normal file
View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="4.8" />
<requires lib="adw" version="1.2" />
<template class="McgAppWindow" parent="AdwApplicationWindow">
<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>
</property>
<child>
<object class="GtkSwitch" id="headerbar_button_connect">
<signal name="state-set" handler="on_headerbar_connection_state_set" swapped="no"/>
</object>
</child>
<child>
<object class="GtkToggleButton" id="headerbar_button_playpause">
<property name="tooltip-text" translatable="yes">Switch between play and pause</property>
<signal name="toggled" handler="on_headerbar_playpause_toggled" swapped="no"/>
<child>
<object class="GtkImage">
<property name="can-focus">False</property>
<property name="icon-name">media-playback-start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkVolumeButton" id="headerbar_button_volume">
<signal name="value-changed" handler="on_headerbar_volume_changed" swapped="no"/>
</object>
</child>
<child type="end">
<object class="GtkStack" id="toolbar_stack">
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkBox" id="content_box">
<property name="orientation">vertical</property>
<child>
<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>
<signal name="notify::visible-child" handler="on_stack_switched" swapped="no"/>
</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>
</object>
</property>
</object>
</property>
</template>
</interface>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/xyz/suruatoel/mcg">
<file>gtk.css</file>
<file>noise-texture.png</file>
<file>ui/window.ui</file>
<file>ui/shortcuts-dialog.ui</file>
<file>ui/connection-panel.ui</file>
<file>ui/album-headerbar.ui</file>
<file>ui/server-panel.ui</file>
<file>ui/cover-panel.ui</file>
<file>ui/playlist-panel.ui</file>
<file>ui/library-panel.ui</file>
</gresource>
</gresources>

View file

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<schemalist> <schemalist>
<enum id="de.coderkun.mcg.SortOrder"> <enum id="xyz.suruatoel.mcg.SortOrder">
<value nick="artist" value="0" /> <value nick="artist" value="0" />
<value nick="title" value="1" /> <value nick="title" value="1" />
<value nick="year" value="2" /> <value nick="year" value="2" />
<value nick="modified" value="3" />
</enum> </enum>
<schema path="/de/coderkun/mcg/" id="de.coderkun.mcg" gettext-domain="mcg"> <schema path="/xyz/suruatoel/mcg/" id="xyz.suruatoel.mcg" gettext-domain="mcg">
<key type="s" name="host"> <key type="s" name="host">
<default>'localhost'</default> <default>'localhost'</default>
<summary>MPD host</summary> <summary>MPD host</summary>
@ -16,11 +17,6 @@
<summary>MPD port</summary> <summary>MPD port</summary>
<description>MPD port to connect to</description> <description>MPD port to connect to</description>
</key> </key>
<key type="s" name="image-dir">
<default>''</default>
<summary>Image directory</summary>
<description>Directory which a webserver is providing images on</description>
</key>
<key type="b" name="connected"> <key type="b" name="connected">
<default>false</default> <default>false</default>
<summary>Connection state</summary> <summary>Connection state</summary>
@ -53,7 +49,7 @@
<summary>Size of library items</summary> <summary>Size of library items</summary>
<description>The size of items displayed in the library.</description> <description>The size of items displayed in the library.</description>
</key> </key>
<key enum="de.coderkun.mcg.SortOrder" name="sort-order"> <key enum="xyz.suruatoel.mcg.SortOrder" name="sort-order">
<default>'year'</default> <default>'year'</default>
<summary>Sort criterium for library items</summary> <summary>Sort criterium for library items</summary>
<description>The sort criterium of items displayed in the library.</description> <description>The sort criterium of items displayed in the library.</description>

Binary file not shown.

View file

@ -1,311 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"POT-Creation-Date: 2019-02-16 23:50+0100\n"
"PO-Revision-Date: 2019-02-16 23:51+0100\n"
"Last-Translator: coderkun <olli@suruatoel.xyz>\n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.1\n"
"X-Poedit-Basepath: ../../..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: data/gtk.glade\n"
"X-Poedit-SearchPath-1: data/gtk.menu.ui\n"
"X-Poedit-SearchPath-2: data/gtk.shortcuts.ui\n"
"X-Poedit-SearchPath-3: mcg\n"
#: data/gtk.glade:32 data/gtk.glade:88
msgid "Title"
msgstr "Titel"
#: data/gtk.glade:49 data/gtk.glade:105
msgid "Artist"
msgstr "Künstler"
#: data/gtk.glade:188
msgid "Sort"
msgstr "Sortierung"
#: data/gtk.glade:198
msgid "sort by artist"
msgstr "nach Künstler"
#: data/gtk.glade:214
msgid "sort by title"
msgstr "nach Titel"
#: data/gtk.glade:230
msgid "sort by year"
msgstr "nach Jahr"
#: data/gtk.glade:292 data/gtk.shortcuts.ui:74
msgid "Connect or disconnect"
msgstr "Die Verbindung herstellen oder trennen"
#: data/gtk.glade:314 data/gtk.shortcuts.ui:81
msgid "Switch between play and pause"
msgstr "Zwischen Abspielen und Pause wechseln"
#: data/gtk.glade:334
msgid "Adjust the volume"
msgstr "Die Lautstärke anpassen"
#: data/gtk.glade:378
msgid "Connect to MPD"
msgstr "Zu MPD verbinden"
#: data/gtk.glade:420 data/gtk.shortcuts.ui:101
msgid "Show the cover in fullscreen mode"
msgstr "Das Cover im Vollbildmodus anzeigen"
#: data/gtk.glade:469 data/gtk.glade:548
msgid "Select multiple albums"
msgstr "Mehrere Alben auswählen"
#: data/gtk.glade:491 data/gtk.shortcuts.ui:88
msgid "Clear the playlist"
msgstr "Die Wiedergabeliste leeren"
#: data/gtk.glade:525 data/gtk.shortcuts.ui:114
msgid "Search the library"
msgstr "Die Bibliothek durchsuchen"
#: data/gtk.glade:570
msgid "Settings and actions"
msgstr "Einstellungen und Aktionen"
#: data/gtk.glade:675
msgid "Enter hostname or IP address"
msgstr "Hostnamen oder IP-Adresse eingeben"
#: data/gtk.glade:687
msgid "Enter URL or local path"
msgstr "URL oder lokalen Pfad eingeben"
#: data/gtk.glade:700
msgid "Enter password or leave blank"
msgstr "Passwort eingeben oder leer lassen"
#: data/gtk.glade:728
msgid "Host:"
msgstr "Host:"
#: data/gtk.glade:740
msgid "Port:"
msgstr "Port:"
#: data/gtk.glade:752
msgid "Password:"
msgstr "Passwort:"
#: data/gtk.glade:764
msgid "Image Directory:"
msgstr "Bildordner:"
#: data/gtk.glade:865
msgid "File:"
msgstr "Datei:"
#: data/gtk.glade:878
msgid "Audio:"
msgstr "Audio:"
#: data/gtk.glade:891
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/gtk.glade:904
msgid "Error:"
msgstr "Fehler:"
#: data/gtk.glade:916 data/gtk.glade:932 data/gtk.glade:948 data/gtk.glade:964
msgid "<i>none</i>"
msgstr "<i>nichts</i>"
#: data/gtk.glade:999
msgid "Status"
msgstr "Status"
#: data/gtk.glade:1084
msgid "Albums"
msgstr "Alben"
#: data/gtk.glade:1096
msgid "Songs"
msgstr "Songs"
#: data/gtk.glade:1108
msgid "Artists"
msgstr "Künstler"
#: data/gtk.glade:1132
msgid "Seconds"
msgstr "Sekunden"
#: data/gtk.glade:1179
msgid "Seconds played"
msgstr "Sekunden gespielt"
#: data/gtk.glade:1190
msgid "Seconds running"
msgstr "Sekunden laufend"
#: data/gtk.glade:1221
msgid "Statistics"
msgstr "Statistiken"
#: data/gtk.glade:1291
msgid "Audio Devices"
msgstr "Audiogeräte"
#: data/gtk.glade:1305
msgid "Server"
msgstr "Server"
#: data/gtk.glade:1486 data/gtk.menu.ui:30
msgid "Cover"
msgstr "Cover"
#: data/gtk.glade:1628 data/gtk.menu.ui:36
msgid "Playlist"
msgstr "Wiedergabeliste"
#: data/gtk.glade:1655
msgid "search library"
msgstr "Bibliothek durchsuchen"
#: data/gtk.glade:1713
msgid "{} of {} images loaded"
msgstr "{} von {} Bildern geladen"
#: data/gtk.glade:1891 data/gtk.menu.ui:42
msgid "Library"
msgstr "Bibliothek"
#: data/gtk.glade:1974
msgid ""
"CoverGrid is a client for the Music Player Daemon, focusing on albums "
"instead of single tracks."
msgstr ""
"CoverGrid ist ein ein Client für den Music Player Daemon, der sich auf Alben "
"anstellen von einzelnen Songs fokussiert."
#: data/gtk.menu.ui:7
msgid "Connect"
msgstr "Verbinden"
#: data/gtk.menu.ui:12
msgid "Play"
msgstr "Abspielen"
#: data/gtk.menu.ui:17
msgid "Clear Playlist"
msgstr "Playlist leeren"
#: data/gtk.menu.ui:24
msgid "Connection"
msgstr "Verbindung"
#: data/gtk.menu.ui:50
msgid "Keyboard Shortcuts"
msgstr "Tastenkombinationen"
#: data/gtk.menu.ui:55
msgid "Info"
msgstr "Info"
#: data/gtk.menu.ui:60
msgid "Quit"
msgstr "Beenden"
#: data/gtk.shortcuts.ui:14
msgid "General"
msgstr "Allgemein"
#: data/gtk.shortcuts.ui:19
msgid "Switch to the Connection panel"
msgstr "Zum Verbindungspaneel wechseln"
#: data/gtk.shortcuts.ui:26
msgid "Switch to the Cover panel"
msgstr "Zum Cover-Paneel wechseln"
#: data/gtk.shortcuts.ui:33
msgid "Switch to the Playlist panel"
msgstr "Zum Wiedergabelistenpaneel wechseln"
#: data/gtk.shortcuts.ui:40
msgid "Switch to the Library panel"
msgstr "Zum Bibliothekspaneel wechseln"
#: data/gtk.shortcuts.ui:47
msgid "Show the keyboard shortcuts"
msgstr "Die Tastenkombinationen anzeigen (dieser Dialog)"
#: data/gtk.shortcuts.ui:54
msgid "Open the info dialog"
msgstr "Den Infodialog öffnen"
#: data/gtk.shortcuts.ui:61
msgid "Quit the application"
msgstr "Die Anwendung beenden"
#: data/gtk.shortcuts.ui:69
msgid "Player"
msgstr "Wiedergabeprogramm"
#: data/gtk.shortcuts.ui:96
msgid "Cover Panel"
msgstr "Cover-Paneel"
#: data/gtk.shortcuts.ui:109
msgid "Library Panel"
msgstr "Bibliothekspaneel"
#: mcg/utils.py:62 mcg/utils.py:72
msgid "{} feat. {}"
msgstr "{} mit {}"
#: mcg/widgets.py:1267 mcg/widgets.py:1601
msgid "cancel"
msgstr "abbrechen"
#: mcg/widgets.py:1284 mcg/widgets.py:1618
msgid "play"
msgstr "abspielen"
#: mcg/widgets.py:1287
msgid "remove"
msgstr "entfernen"
#: mcg/widgets.py:1604 mcg/widgets.py:1621
msgid "queue"
msgstr "einreihen"
#~ msgid "Tracklist"
#~ msgstr "Titelliste"
#~ msgid "large tracklist"
#~ msgstr "große Titelliste"
#~ msgid "small tracklist"
#~ msgstr "kleine Titelliste"
#~ msgid "hide tracklist"
#~ msgstr "gar keine Titelliste"
#~ msgid "_Play"
#~ msgstr "_Abspielen"
#~ msgid "_Keyboard Shortcuts"
#~ msgstr "Tastenkombinationen"
#~ msgid "_Quit"
#~ msgstr "_Beenden"
#~ msgid "Show the keyboard shortcuts (this dialog)"
#~ msgstr "Die Tastenkombinationen anzeigen (dieser Dialog)"

Binary file not shown.

View file

@ -1,310 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"POT-Creation-Date: 2019-02-16 23:52+0100\n"
"PO-Revision-Date: 2019-02-16 23:52+0100\n"
"Last-Translator: coderkun <olli@suruatoel.xyz>\n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.1\n"
"X-Poedit-Basepath: ../../..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SearchPath-0: data/gtk.glade\n"
"X-Poedit-SearchPath-1: data/gtk.menu.ui\n"
"X-Poedit-SearchPath-2: data/gtk.shortcuts.ui\n"
"X-Poedit-SearchPath-3: mcg\n"
#: data/gtk.glade:32 data/gtk.glade:88
msgid "Title"
msgstr "Title"
#: data/gtk.glade:49 data/gtk.glade:105
msgid "Artist"
msgstr "Artist"
#: data/gtk.glade:188
msgid "Sort"
msgstr "Sort order"
#: data/gtk.glade:198
msgid "sort by artist"
msgstr "by Artist"
#: data/gtk.glade:214
msgid "sort by title"
msgstr "by Title"
#: data/gtk.glade:230
msgid "sort by year"
msgstr "by Year"
#: data/gtk.glade:292 data/gtk.shortcuts.ui:74
msgid "Connect or disconnect"
msgstr "Connect or disconnect"
#: data/gtk.glade:314 data/gtk.shortcuts.ui:81
msgid "Switch between play and pause"
msgstr "Switch between play and pause"
#: data/gtk.glade:334
msgid "Adjust the volume"
msgstr "Adjust the volume"
#: data/gtk.glade:378
msgid "Connect to MPD"
msgstr "Connect to MPD"
#: data/gtk.glade:420 data/gtk.shortcuts.ui:101
msgid "Show the cover in fullscreen mode"
msgstr "Show the cover in fullscreen mode"
#: data/gtk.glade:469 data/gtk.glade:548
msgid "Select multiple albums"
msgstr "Select multiple albums"
#: data/gtk.glade:491 data/gtk.shortcuts.ui:88
msgid "Clear the playlist"
msgstr "Clear the playlist"
#: data/gtk.glade:525 data/gtk.shortcuts.ui:114
msgid "Search the library"
msgstr "Search the library"
#: data/gtk.glade:570
msgid "Settings and actions"
msgstr "Settings and actions"
#: data/gtk.glade:675
msgid "Enter hostname or IP address"
msgstr "Enter hostname or IP address"
#: data/gtk.glade:687
msgid "Enter URL or local path"
msgstr "Enter URL or local path"
#: data/gtk.glade:700
msgid "Enter password or leave blank"
msgstr "Enter password or leave blank"
#: data/gtk.glade:728
msgid "Host:"
msgstr "Host:"
#: data/gtk.glade:740
msgid "Port:"
msgstr "Port:"
#: data/gtk.glade:752
msgid "Password:"
msgstr "Password:"
#: data/gtk.glade:764
msgid "Image Directory:"
msgstr "Image Directory:"
#: data/gtk.glade:865
msgid "File:"
msgstr "File:"
#: data/gtk.glade:878
msgid "Audio:"
msgstr "Audio:"
#: data/gtk.glade:891
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/gtk.glade:904
msgid "Error:"
msgstr "Error:"
#: data/gtk.glade:916 data/gtk.glade:932 data/gtk.glade:948 data/gtk.glade:964
msgid "<i>none</i>"
msgstr "<i>none</i>"
#: data/gtk.glade:999
msgid "Status"
msgstr "Status"
#: data/gtk.glade:1084
msgid "Albums"
msgstr "Albums"
#: data/gtk.glade:1096
msgid "Songs"
msgstr "Songs"
#: data/gtk.glade:1108
msgid "Artists"
msgstr "Artists"
#: data/gtk.glade:1132
msgid "Seconds"
msgstr "Seconds"
#: data/gtk.glade:1179
msgid "Seconds played"
msgstr "Seconds"
#: data/gtk.glade:1190
msgid "Seconds running"
msgstr "Seconds running"
#: data/gtk.glade:1221
msgid "Statistics"
msgstr "Statistics"
#: data/gtk.glade:1291
msgid "Audio Devices"
msgstr "Audio Devices"
#: data/gtk.glade:1305
msgid "Server"
msgstr "Server"
#: data/gtk.glade:1486 data/gtk.menu.ui:30
msgid "Cover"
msgstr "Cover"
#: data/gtk.glade:1628 data/gtk.menu.ui:36
msgid "Playlist"
msgstr "Playlist"
#: data/gtk.glade:1655
msgid "search library"
msgstr "search library"
#: data/gtk.glade:1713
msgid "{} of {} images loaded"
msgstr "{} of {} images loaded"
#: data/gtk.glade:1891 data/gtk.menu.ui:42
msgid "Library"
msgstr "Library"
#: data/gtk.glade:1974
msgid ""
"CoverGrid is a client for the Music Player Daemon, focusing on albums "
"instead of single tracks."
msgstr ""
"CoverGrid is a client for the Music Player Daemon, focusing on albums "
"instead of single tracks."
#: data/gtk.menu.ui:7
msgid "Connect"
msgstr "Connect"
#: data/gtk.menu.ui:12
msgid "Play"
msgstr "Play"
#: data/gtk.menu.ui:17
msgid "Clear Playlist"
msgstr "Clear Playlist"
#: data/gtk.menu.ui:24
msgid "Connection"
msgstr "Connection"
#: data/gtk.menu.ui:50
msgid "Keyboard Shortcuts"
msgstr "Keyboard Shortcuts"
#: data/gtk.menu.ui:55
msgid "Info"
msgstr "Info"
#: data/gtk.menu.ui:60
msgid "Quit"
msgstr "Quit"
#: data/gtk.shortcuts.ui:14
msgid "General"
msgstr "General"
#: data/gtk.shortcuts.ui:19
msgid "Switch to the Connection panel"
msgstr "Switch to the Connection panel"
#: data/gtk.shortcuts.ui:26
msgid "Switch to the Cover panel"
msgstr "Switch to the Cover panel"
#: data/gtk.shortcuts.ui:33
msgid "Switch to the Playlist panel"
msgstr "Switch to the Playlist panel"
#: data/gtk.shortcuts.ui:40
msgid "Switch to the Library panel"
msgstr "Switch to the Cover panel"
#: data/gtk.shortcuts.ui:47
msgid "Show the keyboard shortcuts"
msgstr "Show the keyboard shortcuts (this dialog)"
#: data/gtk.shortcuts.ui:54
msgid "Open the info dialog"
msgstr "Open the info dialog"
#: data/gtk.shortcuts.ui:61
msgid "Quit the application"
msgstr "Quit the application"
#: data/gtk.shortcuts.ui:69
msgid "Player"
msgstr "Player"
#: data/gtk.shortcuts.ui:96
msgid "Cover Panel"
msgstr "Cover Panel"
#: data/gtk.shortcuts.ui:109
msgid "Library Panel"
msgstr "Library Panel"
#: mcg/utils.py:62 mcg/utils.py:72
msgid "{} feat. {}"
msgstr "{} feat. {}"
#: mcg/widgets.py:1267 mcg/widgets.py:1601
msgid "cancel"
msgstr "cancel"
#: mcg/widgets.py:1284 mcg/widgets.py:1618
msgid "play"
msgstr "play"
#: mcg/widgets.py:1287
msgid "remove"
msgstr "remove"
#: mcg/widgets.py:1604 mcg/widgets.py:1621
msgid "queue"
msgstr "queue"
#~ msgid "Tracklist"
#~ msgstr "Tracklist"
#~ msgid "large tracklist"
#~ msgstr "large tracklist"
#~ msgid "small tracklist"
#~ msgstr "small tracklist"
#~ msgid "hide tracklist"
#~ msgstr "hide tracklist"
#~ msgid "Show the keyboard shortcuts (this dialog)"
#~ msgstr "Show the keyboard shortcuts (this dialog)"
#~ msgid "_Play"
#~ msgstr "Play"
#~ msgid "_Keyboard Shortcuts"
#~ msgstr "_Keyboard Shortcuts"
#~ msgid "_Quit"
#~ msgstr "_Quit"

View file

@ -1,157 +0,0 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import gettext
import logging
import urllib
from gi.repository import Gio, Gtk, Gdk, GLib
from mcg import Environment
from mcg import widgets
class Application(Gtk.Application):
TITLE = "CoverGrid"
ID = 'de.coderkun.mcg'
DOMAIN = 'mcg'
def _get_option(shortname, longname, description):
option = GLib.OptionEntry()
option.short_name = ord(shortname)
option.long_name = longname
option.description = description
return option
def __init__(self):
Gtk.Application.__init__(self, application_id=Application.ID, flags=Gio.ApplicationFlags.FLAGS_NONE)
self._window = None
self._shortcuts_window = None
self._info_dialog = None
self._verbosity = logging.WARNING
self.add_main_option_entries([
Application._get_option("v", "verbose", "Be verbose: show info messages"),
Application._get_option("d", "debug", "Enable debugging: show debug messages")
])
self.connect('handle-local-options', self.handle_local_options)
def handle_local_options(self, widget, options):
if options.contains("debug") and options.lookup_value('debug'):
self._verbosity = logging.DEBUG
elif options.contains("verbose") and options.lookup_value('verbose'):
self._verbosity = logging.INFO
return -1
def do_startup(self):
Gtk.Application.do_startup(self)
self._setup_logging()
self._load_resource()
self._load_settings()
self._set_default_settings()
self._load_css()
self._setup_locale()
self._load_ui()
self._setup_actions()
self._load_appmenu()
def do_activate(self):
Gtk.Application.do_activate(self)
if not self._window:
self._window = widgets.Window(self, self._builder, Application.TITLE, self._settings)
self._window.present()
def on_menu_shortcuts(self, action, value):
builder = Gtk.Builder()
builder.set_translation_domain(Application.DOMAIN)
builder.add_from_resource(self._get_resource_path('gtk.shortcuts.ui'))
shortcuts_dialog = widgets.ShortcutsDialog(builder, self._window)
shortcuts_dialog.present()
def on_menu_info(self, action, value):
if not self._info_dialog:
self._info_dialog = widgets.InfoDialog(self._builder)
self._info_dialog.run()
def on_menu_quit(self, action, value):
self.quit()
def _setup_logging(self):
logging.basicConfig(
level=self._verbosity,
format="%(asctime)s %(levelname)s: %(message)s"
)
def _load_resource(self):
self._resource = Gio.resource_load(
Environment.get_data(Application.ID + '.gresource')
)
Gio.Resource._register(self._resource)
def _load_settings(self):
self._settings = Gio.Settings.new(Application.ID)
def _set_default_settings(self):
settings = Gtk.Settings.get_default()
settings.set_property('gtk-application-prefer-dark-theme', True)
def _load_css(self):
styleProvider = Gtk.CssProvider()
styleProvider.load_from_resource(self._get_resource_path('gtk.css'))
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
styleProvider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def _setup_locale(self):
relpath = Environment.get_locale()
gettext.bindtextdomain(Application.DOMAIN, relpath)
gettext.textdomain(Application.DOMAIN)
def _load_ui(self):
# Create builder to load UI
self._builder = Gtk.Builder()
self._builder.set_translation_domain(Application.DOMAIN)
self._builder.add_from_resource(self._get_resource_path('gtk.glade'))
def _setup_actions(self):
action = Gio.SimpleAction.new("shortcuts", None)
action.connect('activate', self.on_menu_shortcuts)
self.add_action(action)
action = Gio.SimpleAction.new("info", None)
action.connect('activate', self.on_menu_info)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect('activate', self.on_menu_quit)
self.add_action(action)
def _load_appmenu(self):
builder = Gtk.Builder()
builder.set_translation_domain(Application.DOMAIN)
builder.add_from_resource(self._get_resource_path('gtk.menu.ui'))
self.set_app_menu(builder.get_object('app-menu'))
def _get_resource_path(self, path):
return "/{}/{}".format(Application.ID.replace('.', '/'), path)

View file

@ -1,19 +0,0 @@
#!/usr/bin/env python3
import sys
from mcg.application import Application
def main():
# Start application
app = Application()
exit_status = app.run(sys.argv)
sys.exit(exit_status)
if __name__ == "__main__":
main()

View file

@ -1,93 +0,0 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import hashlib
import locale
import os
import urllib
from gi.repository import GdkPixbuf
class Utils:
def load_cover(url):
if not url:
return None
if url.startswith('/'):
try:
return GdkPixbuf.Pixbuf.new_from_file(url)
except Exception as e:
print(e)
return None
else:
try:
response = urllib.request.urlopen(url)
loader = GdkPixbuf.PixbufLoader()
loader.write(response.read())
loader.close()
return loader.get_pixbuf()
except Exception as e:
print(e)
return None
def load_thumbnail(cache, album, size):
cache_url = cache.create_filename(album)
pixbuf = None
if os.path.isfile(cache_url):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url)
except Exception as e:
print(e)
else:
url = album.get_cover()
pixbuf = Utils.load_cover(url)
if pixbuf is not None:
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER)
filetype = os.path.splitext(url)[1][1:]
if filetype == 'jpg':
filetype = 'jpeg'
pixbuf.savev(cache.create_filename(album), filetype, [], [])
return pixbuf
def create_artists_label(album):
label = ', '.join(album.get_albumartists())
if album.get_artists():
label = locale.gettext("{} feat. {}").format(
label,
", ".join(album.get_artists())
)
return label
def create_track_title(track):
title = track.get_title()
if track.get_artists():
title = locale.gettext("{} feat. {}").format(
title,
", ".join(track.get_artists())
)
return title
def generate_id(values):
if type(values) is not list:
values = [values]
m = hashlib.md5()
for value in values:
m.update(value.encode('utf-8'))
return m.hexdigest()
class SortOrder:
ARTIST = 0
TITLE = 1
YEAR = 2

File diff suppressed because it is too large Load diff

19
meson.build Normal file
View file

@ -0,0 +1,19 @@
project('mcg',
version: '3.2.1',
meson_version: '>= 0.59.0',
default_options: [
'warning_level=2',
'werror=false',
],
)
subdir('data')
subdir('src')
subdir('po')
gnome = import('gnome')
gnome.post_install(
glib_compile_schemas: true,
gtk_update_icon_cache: true,
update_desktop_database: true,
)

1
po/LINGUAS Normal file
View file

@ -0,0 +1 @@
en de

21
po/POTFILES Normal file
View file

@ -0,0 +1,21 @@
data/ui/album-headerbar.ui
data/ui/connection-panel.ui
data/ui/cover-panel.ui
data/ui/library-panel.ui
data/ui/playlist-panel.ui
data/ui/server-panel.ui
data/ui/shortcuts-dialog.ui
data/ui/window.ui
src/albumheaderbar.py
src/application.py
src/client.py
src/connectionpanel.py
src/coverpanel.py
src/librarypanel.py
src/main.py
src/playlistpanel.py
src/serverpanel.py
src/shortcutsdialog.py
src/utils.py
src/window.py
src/zeroconf.py

BIN
po/de.mo Normal file

Binary file not shown.

330
po/de.po Normal file
View file

@ -0,0 +1,330 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-22 14:39+0200\n"
"PO-Revision-Date: 2024-05-22 14:39+0200\n"
"Last-Translator: coderkun <olli@suruatoel.xyz>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.2\n"
"X-Poedit-Basepath: ../../..\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: data/ui/connection-panel.ui:29
msgid "No service found"
msgstr "Keine Dienste gefunden"
#: data/ui/connection-panel.ui:43
msgid "Host"
msgstr "Host"
#: data/ui/connection-panel.ui:56
msgid "Port"
msgstr "Port"
#: data/ui/connection-panel.ui:76
msgid "Password"
msgstr "Passwort"
#: data/ui/cover-panel.ui:12 data/ui/shortcuts-dialog.ui:88
msgid "Show the cover in fullscreen mode"
msgstr "Das Cover im Vollbildmodus anzeigen"
#: data/ui/library-panel.ui:34
msgid "update library"
msgstr "Die Bibliothek aktualisieren"
#: data/ui/library-panel.ui:48
msgid "Sort"
msgstr "Sortierung"
#: data/ui/library-panel.ui:53
msgid "sort by artist"
msgstr "nach Künstler"
#: data/ui/library-panel.ui:61
msgid "sort by title"
msgstr "nach Titel"
#: data/ui/library-panel.ui:69
msgid "sort by year"
msgstr "nach Jahr"
#: data/ui/library-panel.ui:76
msgid "sort by modification"
msgstr "nach Änderungsdatum"
#: data/ui/library-panel.ui:84
msgid "sort library descending"
msgstr "absteigend sortieren"
#: data/ui/library-panel.ui:105 data/ui/shortcuts-dialog.ui:99
msgid "Search the library"
msgstr "Die Bibliothek durchsuchen"
#: data/ui/library-panel.ui:121 data/ui/playlist-panel.ui:14
msgid "Select multiple albums"
msgstr "Mehrere Alben auswählen"
#: data/ui/library-panel.ui:135
msgid "Settings and actions"
msgstr "Einstellungen und Aktionen"
#: data/ui/library-panel.ui:160
msgid "search library"
msgstr "Bibliothek durchsuchen"
#: data/ui/library-panel.ui:263 data/ui/playlist-panel.ui:107
msgid "cancel"
msgstr "abbrechen"
#: data/ui/library-panel.ui:270 data/ui/library-panel.ui:327
msgid "queue"
msgstr "einreihen"
#: data/ui/library-panel.ui:320 data/ui/playlist-panel.ui:163
msgid "play"
msgstr "abspielen"
#: data/ui/playlist-panel.ui:28 data/ui/shortcuts-dialog.ui:77
msgid "Clear the playlist"
msgstr "Die Wiedergabeliste leeren"
#: data/ui/playlist-panel.ui:114 data/ui/playlist-panel.ui:170
msgid "remove"
msgstr "entfernen"
#: data/ui/server-panel.ui:35
msgid "Status"
msgstr "Status"
#: data/ui/server-panel.ui:49
msgid "File:"
msgstr "Datei:"
#: data/ui/server-panel.ui:60
msgid "Audio:"
msgstr "Audio:"
#: data/ui/server-panel.ui:71
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/ui/server-panel.ui:82
msgid "Error:"
msgstr "Fehler:"
#: data/ui/server-panel.ui:92 data/ui/server-panel.ui:106
#: data/ui/server-panel.ui:120 data/ui/server-panel.ui:134
msgid "<i>none</i>"
msgstr "<i>nichts</i>"
#: data/ui/server-panel.ui:154
msgid "Statistics"
msgstr "Statistiken"
#: data/ui/server-panel.ui:178
msgid "Artists"
msgstr "Künstler"
#: data/ui/server-panel.ui:198
msgid "Albums"
msgstr "Alben"
#: data/ui/server-panel.ui:218
msgid "Songs"
msgstr "Songs"
#: data/ui/server-panel.ui:238
msgid "Seconds"
msgstr "Sekunden"
#: data/ui/server-panel.ui:267
msgid "Seconds played"
msgstr "Sekunden gespielt"
#: data/ui/server-panel.ui:287
msgid "Seconds running"
msgstr "Sekunden laufend"
#: data/ui/server-panel.ui:303
msgid "Audio Devices"
msgstr "Audiogeräte"
#: data/ui/shortcuts-dialog.ui:14
msgid "General"
msgstr "Allgemein"
#: data/ui/shortcuts-dialog.ui:18
msgid "Switch to the Connection panel"
msgstr "Zum Verbindungspaneel wechseln"
#: data/ui/shortcuts-dialog.ui:24
msgid "Switch to the Cover panel"
msgstr "Zum Cover-Paneel wechseln"
#: data/ui/shortcuts-dialog.ui:30
msgid "Switch to the Playlist panel"
msgstr "Zum Wiedergabelistenpaneel wechseln"
#: data/ui/shortcuts-dialog.ui:36
msgid "Switch to the Library panel"
msgstr "Zum Bibliothekspaneel wechseln"
#: data/ui/shortcuts-dialog.ui:42
msgid "Show the keyboard shortcuts"
msgstr "Die Tastenkombinationen anzeigen (dieser Dialog)"
#: data/ui/shortcuts-dialog.ui:48
msgid "Open the info dialog"
msgstr "Den Infodialog öffnen"
#: data/ui/shortcuts-dialog.ui:54
msgid "Quit the application"
msgstr "Die Anwendung beenden"
#: data/ui/shortcuts-dialog.ui:61
msgid "Player"
msgstr "Wiedergabeprogramm"
#: data/ui/shortcuts-dialog.ui:65
msgid "Connect or disconnect"
msgstr "Die Verbindung herstellen oder trennen"
#: data/ui/shortcuts-dialog.ui:71 data/ui/window.ui:25
msgid "Switch between play and pause"
msgstr "Zwischen Abspielen und Pause wechseln"
#: data/ui/shortcuts-dialog.ui:84
msgid "Cover Panel"
msgstr "Cover-Paneel"
#: data/ui/shortcuts-dialog.ui:95
msgid "Library Panel"
msgstr "Bibliothekspaneel"
#: src/connectionpanel.py:51
msgid "use"
msgstr "verwenden"
#: src/librarypanel.py:291
msgid "Loading albums"
msgstr "Alben werden geladen"
#: src/librarypanel.py:379
msgid "Loading images"
msgstr "Bilder werden geladen"
#: src/utils.py:50 src/utils.py:67
msgid "{} feat. {}"
msgstr "{} mit {}"
#: src/utils.py:61
msgid "{}:{} minutes"
msgstr "{}:{} Minuten"
#: src/window.py:114
msgid "Server"
msgstr "Server"
#: src/window.py:115
msgid "Cover"
msgstr "Cover"
#: src/window.py:116
msgid "Playlist"
msgstr "Wiedergabeliste"
#: src/window.py:117
msgid "Library"
msgstr "Bibliothek"
#, fuzzy
#~ msgid "Connection state"
#~ msgstr "Verbindung"
#~ msgid "Title"
#~ msgstr "Titel"
#~ msgid "Artist"
#~ msgstr "Künstler"
#~ msgid "Enter hostname or IP address"
#~ msgstr "Hostnamen oder IP-Adresse eingeben"
#~ msgid "Enter password or leave blank"
#~ msgstr "Passwort eingeben oder leer lassen"
#~ msgid "Connect"
#~ msgstr "Verbinden"
#~ msgid "Play"
#~ msgstr "Abspielen"
#~ msgid "Clear Playlist"
#~ msgstr "Playlist leeren"
#~ msgid "Toggle Fullscreen"
#~ msgstr "Vollbild wechseln"
#~ msgid "Search Library"
#~ msgstr "Bibliothek durchsuchen"
#~ msgid "Connection"
#~ msgstr "Verbindung"
#~ msgid "Keyboard Shortcuts"
#~ msgstr "Tastenkombinationen"
#~ msgid "Info"
#~ msgstr "Info"
#~ msgid "Quit"
#~ msgstr "Beenden"
#~ msgid "CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks."
#~ msgstr "CoverGrid ist ein ein Client für den Music Player Daemon, der sich auf Alben anstellen von einzelnen Songs fokussiert."
#~ msgid "Connect to MPD"
#~ msgstr "Zu MPD verbinden"
#~ msgid "Adjust the volume"
#~ msgstr "Die Lautstärke anpassen"
#~ msgid "Enter URL or local path"
#~ msgstr "URL oder lokalen Pfad eingeben"
#~ msgid "Image Directory:"
#~ msgstr "Bildordner:"
#~ msgid "{} of {} images loaded"
#~ msgstr "{} von {} Bildern geladen"
#~ msgid "Tracklist"
#~ msgstr "Titelliste"
#~ msgid "large tracklist"
#~ msgstr "große Titelliste"
#~ msgid "small tracklist"
#~ msgstr "kleine Titelliste"
#~ msgid "hide tracklist"
#~ msgstr "gar keine Titelliste"
#~ msgid "_Play"
#~ msgstr "_Abspielen"
#~ msgid "_Keyboard Shortcuts"
#~ msgstr "Tastenkombinationen"
#~ msgid "_Quit"
#~ msgstr "_Beenden"
#~ msgid "Show the keyboard shortcuts (this dialog)"
#~ msgstr "Die Tastenkombinationen anzeigen (dieser Dialog)"

BIN
po/en.mo Normal file

Binary file not shown.

331
po/en.po Normal file
View file

@ -0,0 +1,331 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-22 14:39+0200\n"
"PO-Revision-Date: 2024-05-22 14:39+0200\n"
"Last-Translator: coderkun <olli@suruatoel.xyz>\n"
"Language-Team: \n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.2\n"
"X-Poedit-Basepath: ../../..\n"
"X-Poedit-SearchPath-0: mcg\n"
"X-Poedit-SearchPath-1: data/ui\n"
#: data/ui/connection-panel.ui:29
msgid "No service found"
msgstr "No service found"
#: data/ui/connection-panel.ui:43
msgid "Host"
msgstr "Host"
#: data/ui/connection-panel.ui:56
msgid "Port"
msgstr "Port"
#: data/ui/connection-panel.ui:76
msgid "Password"
msgstr "Password"
#: data/ui/cover-panel.ui:12 data/ui/shortcuts-dialog.ui:88
msgid "Show the cover in fullscreen mode"
msgstr "Show the cover in fullscreen mode"
#: data/ui/library-panel.ui:34
msgid "update library"
msgstr "update the library"
#: data/ui/library-panel.ui:48
msgid "Sort"
msgstr "Sort order"
#: data/ui/library-panel.ui:53
msgid "sort by artist"
msgstr "by Artist"
#: data/ui/library-panel.ui:61
msgid "sort by title"
msgstr "by Title"
#: data/ui/library-panel.ui:69
msgid "sort by year"
msgstr "by Year"
#: data/ui/library-panel.ui:76
msgid "sort by modification"
msgstr "by Modification Date"
#: data/ui/library-panel.ui:84
msgid "sort library descending"
msgstr "sort descending"
#: data/ui/library-panel.ui:105 data/ui/shortcuts-dialog.ui:99
msgid "Search the library"
msgstr "Search the library"
#: data/ui/library-panel.ui:121 data/ui/playlist-panel.ui:14
msgid "Select multiple albums"
msgstr "Select multiple albums"
#: data/ui/library-panel.ui:135
msgid "Settings and actions"
msgstr "Settings and actions"
#: data/ui/library-panel.ui:160
msgid "search library"
msgstr "search library"
#: data/ui/library-panel.ui:263 data/ui/playlist-panel.ui:107
msgid "cancel"
msgstr "cancel"
#: data/ui/library-panel.ui:270 data/ui/library-panel.ui:327
msgid "queue"
msgstr "queue"
#: data/ui/library-panel.ui:320 data/ui/playlist-panel.ui:163
msgid "play"
msgstr "play"
#: data/ui/playlist-panel.ui:28 data/ui/shortcuts-dialog.ui:77
msgid "Clear the playlist"
msgstr "Clear the playlist"
#: data/ui/playlist-panel.ui:114 data/ui/playlist-panel.ui:170
msgid "remove"
msgstr "remove"
#: data/ui/server-panel.ui:35
msgid "Status"
msgstr "Status"
#: data/ui/server-panel.ui:49
msgid "File:"
msgstr "File:"
#: data/ui/server-panel.ui:60
msgid "Audio:"
msgstr "Audio:"
#: data/ui/server-panel.ui:71
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/ui/server-panel.ui:82
msgid "Error:"
msgstr "Error:"
#: data/ui/server-panel.ui:92 data/ui/server-panel.ui:106
#: data/ui/server-panel.ui:120 data/ui/server-panel.ui:134
msgid "<i>none</i>"
msgstr "<i>none</i>"
#: data/ui/server-panel.ui:154
msgid "Statistics"
msgstr "Statistics"
#: data/ui/server-panel.ui:178
msgid "Artists"
msgstr "Artists"
#: data/ui/server-panel.ui:198
msgid "Albums"
msgstr "Albums"
#: data/ui/server-panel.ui:218
msgid "Songs"
msgstr "Songs"
#: data/ui/server-panel.ui:238
msgid "Seconds"
msgstr "Seconds"
#: data/ui/server-panel.ui:267
msgid "Seconds played"
msgstr "Seconds"
#: data/ui/server-panel.ui:287
msgid "Seconds running"
msgstr "Seconds running"
#: data/ui/server-panel.ui:303
msgid "Audio Devices"
msgstr "Audio Devices"
#: data/ui/shortcuts-dialog.ui:14
msgid "General"
msgstr "General"
#: data/ui/shortcuts-dialog.ui:18
msgid "Switch to the Connection panel"
msgstr "Switch to the Connection panel"
#: data/ui/shortcuts-dialog.ui:24
msgid "Switch to the Cover panel"
msgstr "Switch to the Cover panel"
#: data/ui/shortcuts-dialog.ui:30
msgid "Switch to the Playlist panel"
msgstr "Switch to the Playlist panel"
#: data/ui/shortcuts-dialog.ui:36
msgid "Switch to the Library panel"
msgstr "Switch to the Cover panel"
#: data/ui/shortcuts-dialog.ui:42
msgid "Show the keyboard shortcuts"
msgstr "Show the keyboard shortcuts (this dialog)"
#: data/ui/shortcuts-dialog.ui:48
msgid "Open the info dialog"
msgstr "Open the info dialog"
#: data/ui/shortcuts-dialog.ui:54
msgid "Quit the application"
msgstr "Quit the application"
#: data/ui/shortcuts-dialog.ui:61
msgid "Player"
msgstr "Player"
#: data/ui/shortcuts-dialog.ui:65
msgid "Connect or disconnect"
msgstr "Connect or disconnect"
#: data/ui/shortcuts-dialog.ui:71 data/ui/window.ui:25
msgid "Switch between play and pause"
msgstr "Switch between play and pause"
#: data/ui/shortcuts-dialog.ui:84
msgid "Cover Panel"
msgstr "Cover Panel"
#: data/ui/shortcuts-dialog.ui:95
msgid "Library Panel"
msgstr "Library Panel"
#: src/connectionpanel.py:51
msgid "use"
msgstr "use"
#: src/librarypanel.py:291
msgid "Loading albums"
msgstr "Loading albums"
#: src/librarypanel.py:379
msgid "Loading images"
msgstr "Loading images"
#: src/utils.py:50 src/utils.py:67
msgid "{} feat. {}"
msgstr "{} feat. {}"
#: src/utils.py:61
msgid "{}:{} minutes"
msgstr "{}:{} minutes"
#: src/window.py:114
msgid "Server"
msgstr "Server"
#: src/window.py:115
msgid "Cover"
msgstr "Cover"
#: src/window.py:116
msgid "Playlist"
msgstr "Playlist"
#: src/window.py:117
msgid "Library"
msgstr "Library"
#, fuzzy
#~ msgid "Connection state"
#~ msgstr "Connection"
#~ msgid "Title"
#~ msgstr "Title"
#~ msgid "Artist"
#~ msgstr "Artist"
#~ msgid "Enter hostname or IP address"
#~ msgstr "Enter hostname or IP address"
#~ msgid "Enter password or leave blank"
#~ msgstr "Enter password or leave blank"
#~ msgid "Connect"
#~ msgstr "Connect"
#~ msgid "Play"
#~ msgstr "Play"
#~ msgid "Clear Playlist"
#~ msgstr "Clear Playlist"
#~ msgid "Toggle Fullscreen"
#~ msgstr "Toggle fullscreen"
#~ msgid "Search Library"
#~ msgstr "Search Library"
#~ msgid "Connection"
#~ msgstr "Connection"
#~ msgid "Keyboard Shortcuts"
#~ msgstr "Keyboard Shortcuts"
#~ msgid "Info"
#~ msgstr "Info"
#~ msgid "Quit"
#~ msgstr "Quit"
#~ msgid "CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks."
#~ msgstr "CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks."
#~ msgid "Connect to MPD"
#~ msgstr "Connect to MPD"
#~ msgid "Adjust the volume"
#~ msgstr "Adjust the volume"
#~ msgid "Enter URL or local path"
#~ msgstr "Enter URL or local path"
#~ msgid "Image Directory:"
#~ msgstr "Image Directory:"
#~ msgid "{} of {} images loaded"
#~ msgstr "{} of {} images loaded"
#~ msgid "Tracklist"
#~ msgstr "Tracklist"
#~ msgid "large tracklist"
#~ msgstr "large tracklist"
#~ msgid "small tracklist"
#~ msgstr "small tracklist"
#~ msgid "hide tracklist"
#~ msgstr "hide tracklist"
#~ msgid "Show the keyboard shortcuts (this dialog)"
#~ msgstr "Show the keyboard shortcuts (this dialog)"
#~ msgid "_Play"
#~ msgstr "Play"
#~ msgid "_Keyboard Shortcuts"
#~ msgstr "_Keyboard Shortcuts"
#~ msgid "_Quit"
#~ msgstr "_Quit"

247
po/mcg.pot Normal file
View file

@ -0,0 +1,247 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mcg package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: mcg\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-22 14:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: data/ui/connection-panel.ui:29
msgid "No service found"
msgstr ""
#: data/ui/connection-panel.ui:43
msgid "Host"
msgstr ""
#: data/ui/connection-panel.ui:56
msgid "Port"
msgstr ""
#: data/ui/connection-panel.ui:76
msgid "Password"
msgstr ""
#: data/ui/cover-panel.ui:12 data/ui/shortcuts-dialog.ui:88
msgid "Show the cover in fullscreen mode"
msgstr ""
#: data/ui/library-panel.ui:34
msgid "update library"
msgstr ""
#: data/ui/library-panel.ui:48
msgid "Sort"
msgstr ""
#: data/ui/library-panel.ui:53
msgid "sort by artist"
msgstr ""
#: data/ui/library-panel.ui:61
msgid "sort by title"
msgstr ""
#: data/ui/library-panel.ui:69
msgid "sort by year"
msgstr ""
#: data/ui/library-panel.ui:76
msgid "sort by modification"
msgstr ""
#: data/ui/library-panel.ui:84
msgid "sort library descending"
msgstr ""
#: data/ui/library-panel.ui:105 data/ui/shortcuts-dialog.ui:99
msgid "Search the library"
msgstr ""
#: data/ui/library-panel.ui:121 data/ui/playlist-panel.ui:14
msgid "Select multiple albums"
msgstr ""
#: data/ui/library-panel.ui:135
msgid "Settings and actions"
msgstr ""
#: data/ui/library-panel.ui:160
msgid "search library"
msgstr ""
#: data/ui/library-panel.ui:263 data/ui/playlist-panel.ui:107
msgid "cancel"
msgstr ""
#: data/ui/library-panel.ui:270 data/ui/library-panel.ui:327
msgid "queue"
msgstr ""
#: data/ui/library-panel.ui:320 data/ui/playlist-panel.ui:163
msgid "play"
msgstr ""
#: data/ui/playlist-panel.ui:28 data/ui/shortcuts-dialog.ui:77
msgid "Clear the playlist"
msgstr ""
#: data/ui/playlist-panel.ui:114 data/ui/playlist-panel.ui:170
msgid "remove"
msgstr ""
#: data/ui/server-panel.ui:35
msgid "Status"
msgstr ""
#: data/ui/server-panel.ui:49
msgid "File:"
msgstr ""
#: data/ui/server-panel.ui:60
msgid "Audio:"
msgstr ""
#: data/ui/server-panel.ui:71
msgid "Bitrate:"
msgstr ""
#: data/ui/server-panel.ui:82
msgid "Error:"
msgstr ""
#: data/ui/server-panel.ui:92 data/ui/server-panel.ui:106
#: data/ui/server-panel.ui:120 data/ui/server-panel.ui:134
msgid "<i>none</i>"
msgstr ""
#: data/ui/server-panel.ui:154
msgid "Statistics"
msgstr ""
#: data/ui/server-panel.ui:178
msgid "Artists"
msgstr ""
#: data/ui/server-panel.ui:198
msgid "Albums"
msgstr ""
#: data/ui/server-panel.ui:218
msgid "Songs"
msgstr ""
#: data/ui/server-panel.ui:238
msgid "Seconds"
msgstr ""
#: data/ui/server-panel.ui:267
msgid "Seconds played"
msgstr ""
#: data/ui/server-panel.ui:287
msgid "Seconds running"
msgstr ""
#: data/ui/server-panel.ui:303
msgid "Audio Devices"
msgstr ""
#: data/ui/shortcuts-dialog.ui:14
msgid "General"
msgstr ""
#: data/ui/shortcuts-dialog.ui:18
msgid "Switch to the Connection panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:24
msgid "Switch to the Cover panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:30
msgid "Switch to the Playlist panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:36
msgid "Switch to the Library panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:42
msgid "Show the keyboard shortcuts"
msgstr ""
#: data/ui/shortcuts-dialog.ui:48
msgid "Open the info dialog"
msgstr ""
#: data/ui/shortcuts-dialog.ui:54
msgid "Quit the application"
msgstr ""
#: data/ui/shortcuts-dialog.ui:61
msgid "Player"
msgstr ""
#: data/ui/shortcuts-dialog.ui:65
msgid "Connect or disconnect"
msgstr ""
#: data/ui/shortcuts-dialog.ui:71 data/ui/window.ui:25
msgid "Switch between play and pause"
msgstr ""
#: data/ui/shortcuts-dialog.ui:84
msgid "Cover Panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:95
msgid "Library Panel"
msgstr ""
#: src/connectionpanel.py:51
msgid "use"
msgstr ""
#: src/librarypanel.py:291
msgid "Loading albums"
msgstr ""
#: src/librarypanel.py:379
msgid "Loading images"
msgstr ""
#: src/utils.py:50 src/utils.py:67
msgid "{} feat. {}"
msgstr ""
#: src/utils.py:61
msgid "{}:{} minutes"
msgstr ""
#: src/window.py:114
msgid "Server"
msgstr ""
#: src/window.py:115
msgid "Cover"
msgstr ""
#: src/window.py:116
msgid "Playlist"
msgstr ""
#: src/window.py:117
msgid "Library"
msgstr ""

3
po/meson.build Normal file
View file

@ -0,0 +1,3 @@
i18n = import('i18n')
i18n.gettext(meson.project_name(), preset: 'glib')

115
setup.py
View file

@ -1,115 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import subprocess
from setuptools import setup
from setuptools.command.build_py import build_py
from setuptools.dist import Distribution
class MCGDistribution(Distribution):
global_options = Distribution.global_options + [
("no-compile-schemas", None, "Don't compile gsettings schemas")
]
def __init__(self, *args, **kwargs):
self.no_compile_schemas = False
super(self.__class__, self).__init__(*args, **kwargs)
class build_mcg(build_py):
def run(self, *args, **kwargs):
super(self.__class__, self).run(*args, **kwargs)
self._build_gresources()
if not self.distribution.no_compile_schemas:
self._build_gschemas()
def _build_gresources(self):
print("compiling gresources")
subprocess.run(['glib-compile-resources', 'de.coderkun.mcg.gresource.xml'], cwd='data')
def _build_gschemas(self):
print("compiling gschemas")
subprocess.run(['glib-compile-schemas', 'data'])
setup(
distclass = MCGDistribution,
cmdclass = {
'build_py': build_mcg
},
name = "mcg",
version = '2.0.1',
description = "CoverGrid (mcg) is a client for the Music Player Daemon, focusing on albums instead of single tracks.",
url = "http://www.suruatoel.xyz/codes/mcg",
author = "coderkun",
author_email = "olli@suruatoel.xyz",
license = "GPL",
packages = [
'mcg',
'mcg/data'
],
package_dir = {
'mcg': 'mcg',
'mcg/data': 'data'
},
package_data = {
'mcg': [
'LICENSE',
'README.textile'
],
'mcg/data': [
'de.coderkun.mcg.gresource'
]
},
extras_require = {
'keyring support': ["python-keyring"]
},
entry_points = {
"gui_scripts": [
"mcg = mcg.mcg:main"
]
},
data_files = [
(os.path.join('share', 'applications'), [
"data/mcg.desktop"
]),
(os.path.join('share', 'icons'), [
"data/mcg.svg"
]),
(os.path.join('share', 'glib-2.0', 'schemas'), [
"data/de.coderkun.mcg.gschema.xml"
]),
(os.path.join('share', 'locale', 'en', 'LC_MESSAGES'), [
'locale/en/LC_MESSAGES/mcg.mo'
]),
(os.path.join('share', 'locale', 'de', 'LC_MESSAGES'), [
'locale/de/LC_MESSAGES/mcg.mo'
])
],
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: X11 Applications :: GTK"
"Intended Audience :: End Users/Desktop"
"License :: OSI Approved :: GNU General Public License (GPL)"
"Operating System :: OS Independent"
"Programming Language :: Python :: 3"
"Topic :: Desktop Environment :: Gnome"
"Topic :: Multimedia :: Sound/Audio"
"Topic :: Multimedia :: Sound/Audio :: Players"
]
)

36
src/albumheaderbar.py Normal file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, GObject, Adw
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/album-headerbar.ui')
class AlbumHeaderbar(Adw.Bin):
__gtype_name__ = 'McgAlbumHeaderbar'
__gsignals__ = {
'close': (GObject.SIGNAL_RUN_FIRST, None, ())
}
# Widgets
standalone_title = Gtk.Template.Child()
standalone_artist = Gtk.Template.Child()
def __init__(self):
super().__init__()
@Gtk.Template.Callback()
def on_close_clicked(self, widget):
self.emit('close')
def set_album(self, album):
self.standalone_title.set_text(album.get_title())
self.standalone_artist.set_text(", ".join(album.get_albumartists()))

124
src/application.py Normal file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env python3
import logging
import urllib
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gio, Gtk, Gdk, GLib, Adw
from .window import Window
class Application(Gtk.Application):
TITLE = "CoverGrid"
ID = 'xyz.suruatoel.mcg'
DOMAIN = 'mcg'
def __init__(self):
super().__init__(application_id=Application.ID, flags=Gio.ApplicationFlags.FLAGS_NONE)
self._window = None
self._info_dialog = None
self._verbosity = logging.WARNING
self.set_accels_for_action('window.close', ['<primary>q'])
self.set_accels_for_action('win.show-help-overlay', ['<primary>k'])
self.set_accels_for_action('app.info', ['<primary>i'])
self.set_accels_for_action('win.connect', ['<primary>c'])
self.set_accels_for_action('win.play', ['<primary>p'])
self.set_accels_for_action('win.clear-playlist', ['<primary>r'])
self.set_accels_for_action('win.toggle-fullscreen', ['F11'])
self.set_accels_for_action('win.search-library', ['<primary>f'])
self.set_accels_for_action('win.panel("0")', ['<primary>KP_1'])
self.set_accels_for_action('win.panel("1")', ['<primary>KP_2'])
self.set_accels_for_action('win.panel("2")', ['<primary>KP_3'])
self.set_accels_for_action('win.panel("3")', ['<primary>KP_4'])
def do_startup(self):
Gtk.Application.do_startup(self)
self._setup_logging()
self._load_settings()
self._set_default_settings()
self._load_css()
self._setup_actions()
self._setup_adw()
def do_activate(self):
Gtk.Application.do_activate(self)
if not self._window:
self._window = Window(self, Application.TITLE, self._settings)
self._window.present()
def on_menu_info(self, action, value):
self._info_dialog = Adw.AboutDialog()
self._info_dialog.set_application_icon("xyz.suruatoel.mcg")
self._info_dialog.set_application_name("CoverGrid")
self._info_dialog.set_version("3.2.1")
self._info_dialog.set_comments("CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.")
self._info_dialog.set_website("https://www.suruatoel.xyz/codes/mcg")
self._info_dialog.set_license_type(Gtk.License.GPL_3_0)
self._info_dialog.set_issue_url("https://git.suruatoel.xyz/coderkun/mcg")
self._info_dialog.present()
def on_menu_quit(self, action, value):
self.quit()
def _setup_logging(self):
logging.basicConfig(
level=self._verbosity,
format="%(asctime)s %(levelname)s: %(message)s"
)
def _load_settings(self):
self._settings = Gio.Settings.new(Application.ID)
def _set_default_settings(self):
style_manager = Adw.StyleManager.get_default()
style_manager.set_color_scheme(Adw.ColorScheme.PREFER_DARK)
def _load_css(self):
styleProvider = Gtk.CssProvider()
styleProvider.load_from_resource(self._get_resource_path('gtk.css'))
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
styleProvider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def _setup_actions(self):
action = Gio.SimpleAction.new("info", None)
action.connect('activate', self.on_menu_info)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect('activate', self.on_menu_quit)
self.add_action(action)
def _get_resource_path(self, path):
return "/{}/{}".format(Application.ID.replace('.', '/'), path)
def _setup_adw(self):
Adw.HeaderBar()
Adw.ToolbarView()
Adw.ViewSwitcherTitle()
Adw.ViewSwitcherBar()
Adw.ViewStackPage()
Adw.ToastOverlay()
Adw.StatusPage()
Adw.Flap()
Adw.EntryRow()
Adw.PasswordEntryRow()

View file

@ -1,16 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import concurrent.futures
import configparser import configparser
import glob import dateutil.parser
import logging import logging
import os import os
import queue import queue
import re import re
import socket import socket
import sys
import threading import threading
import urllib.request
from mcg.utils import SortOrder from mcg.utils import SortOrder
from mcg.utils import Utils from mcg.utils import Utils
@ -28,7 +27,7 @@ class MPDException(Exception):
if error: if error:
parts = re.match("\[(\d+)@(\d+)\]\s\{(\w+)\}\s(.*)", error) parts = re.match("\[(\d+)@(\d+)\]\s\{(\w+)\}\s(.*)", error)
if parts: if parts:
self._error = int(parts.group(1)) self._error_number = int(parts.group(1))
self._command_number = int(parts.group(2)) self._command_number = int(parts.group(2))
self._command_name = parts.group(3) self._command_name = parts.group(3)
return parts.group(4) return parts.group(4)
@ -39,6 +38,10 @@ class MPDException(Exception):
return self._error return self._error
def get_error_number(self):
return self._error_number
def get_command_number(self): def get_command_number(self):
return self._command_number return self._command_number
@ -61,6 +64,18 @@ class CommandException(MPDException):
class Future(concurrent.futures.Future):
def __init__(self, signal):
concurrent.futures.Future.__init__(self)
self._signal = signal
def get_signal(self):
return self._signal
class Base(): class Base():
def __init__(self): def __init__(self):
self._callbacks = {} self._callbacks = {}
@ -88,6 +103,10 @@ class Base():
callback(*data) callback(*data)
def _callback_future(self, future):
self._callback(future.get_signal(), *future.result())
class Client(Base): class Client(Base):
@ -104,22 +123,31 @@ class Client(Base):
PROTOCOL_ERROR = 'ACK ' PROTOCOL_ERROR = 'ACK '
# Protocol: error: permission # Protocol: error: permission
PROTOCOL_ERROR_PERMISSION = 4 PROTOCOL_ERROR_PERMISSION = 4
# Protocol: error: no exists
PROTOCOL_ERROR_NOEXISTS = 50
# Signal: connection status # Signal: connection status
SIGNAL_CONNECTION = 'connection' SIGNAL_CONNECTION = 'connection'
# Signal: status # Signal: status
SIGNAL_STATUS = 'status' SIGNAL_STATUS = 'status'
# Signal: stats # Signal: stats
SIGNAL_STATS = 'stats' SIGNAL_STATS = 'stats'
# Signal: init loading of albums
SIGNAL_INIT_ALBUMS = 'init-albums'
# Signal: pulse loading of albums
SIGNAL_PULSE_ALBUMS = 'pulse-albums'
# Signal: load albums # Signal: load albums
SIGNAL_LOAD_ALBUMS = 'load-albums' SIGNAL_LOAD_ALBUMS = 'load-albums'
# Signal: load playlist # Signal: load playlist
SIGNAL_LOAD_PLAYLIST = 'load-playlist' SIGNAL_LOAD_PLAYLIST = 'load-playlist'
# Signal: load audio output devices # Signal: load audio output devices
SIGNAL_LOAD_OUTPUT_DEVICES = 'load-output-devices' SIGNAL_LOAD_OUTPUT_DEVICES = 'load-output-devices'
# Signal: custom (dummy) event to trigger callback # Signal: load albumart
SIGNAL_CUSTOM = 'custom' SIGNAL_LOAD_ALBUMART = 'albumart'
# Signal: error # Signal: error
SIGNAL_ERROR = 'error' SIGNAL_ERROR = 'error'
# Buffer size for reading from socket
SOCKET_BUFSIZE = 4096
def __init__(self): def __init__(self):
@ -127,7 +155,7 @@ class Client(Base):
Base.__init__(self) Base.__init__(self)
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self._sock = None self._sock = None
self._sock_read = None self._buffer = bytearray()
self._sock_write = None self._sock_write = None
self._stop = threading.Event() self._stop = threading.Event()
self._actions = queue.Queue() self._actions = queue.Queue()
@ -136,7 +164,6 @@ class Client(Base):
self._host = None self._host = None
self._albums = {} self._albums = {}
self._playlist = [] self._playlist = []
self._image_dir = ""
self._state = None self._state = None
@ -146,13 +173,12 @@ class Client(Base):
# Client commands # Client commands
def connect(self, host, port, password=None, image_dir=""): def connect(self, host, port, password=None):
"""Connect to MPD with the given host, port and password or with """Connect to MPD with the given host, port and password or with
standard values. standard values.
""" """
self._logger.info("connect") self._logger.info("connect")
self._host = host self._host = host
self._image_dir = image_dir
self._add_action(self._connect, host, port, password) self._add_action(self._connect, host, port, password)
self._stop.clear() self._stop.clear()
self._start_worker() self._start_worker()
@ -177,19 +203,19 @@ class Client(Base):
def get_status(self): def get_status(self):
"""Determine the current status.""" """Determine the current status."""
self._logger.info("get status") self._logger.info("get status")
self._add_action(self._get_status) self._add_action_signal(Client.SIGNAL_STATUS, self._get_status)
def get_stats(self): def get_stats(self):
"""Load statistics.""" """Load statistics."""
self._logger.info("get stats") self._logger.info("get stats")
self._add_action(self._get_stats) self._add_action_signal(Client.SIGNAL_STATS, self._get_stats)
def get_output_devices(self): def get_output_devices(self):
"""Determine the list of audio output devices.""" """Determine the list of audio output devices."""
self._logger.info("get output devices") self._logger.info("get output devices")
self._add_action(self._get_output_devices) self._add_action_signal(Client.SIGNAL_LOAD_OUTPUT_DEVICES, self._get_output_devices)
def enable_output_device(self, device, enabled): def enable_output_device(self, device, enabled):
@ -201,7 +227,7 @@ class Client(Base):
def load_albums(self): def load_albums(self):
self._logger.info("load albums") self._logger.info("load albums")
self._add_action(self._load_albums) self._add_action_signal(Client.SIGNAL_LOAD_ALBUMS, self._load_albums)
def update(self): def update(self):
@ -211,7 +237,7 @@ class Client(Base):
def load_playlist(self): def load_playlist(self):
self._logger.info("load playlist") self._logger.info("load playlist")
self._add_action(self._load_playlist) self._add_action_signal(Client.SIGNAL_LOAD_PLAYLIST, self._load_playlist)
def clear_playlist(self): def clear_playlist(self):
@ -278,9 +304,17 @@ class Client(Base):
self._add_action(self._set_volume, volume) self._add_action(self._set_volume, volume)
def get_custom(self, name): def get_albumart(self, album):
self._logger.info("get custom \"%s\"", name) self._logger.info("get albumart")
self._add_action(self._get_custom, name) self._add_action_signal(Client.SIGNAL_LOAD_ALBUMART, self._get_albumart, album)
def get_albumart_now(self, album):
self._logger.info("get albumart now")
future = concurrent.futures.Future()
self._add_action_future(future, self._get_albumart, album)
(_, albumart) = future.result()
return albumart
# Private methods # Private methods
@ -291,7 +325,6 @@ class Client(Base):
return return
try: try:
self._sock = self._connect_socket(host, port) self._sock = self._connect_socket(host, port)
self._sock_read = self._sock.makefile("r", encoding="utf-8")
self._sock_write = self._sock.makefile("w", encoding="utf-8") self._sock_write = self._sock.makefile("w", encoding="utf-8")
self._greet() self._greet()
self._logger.info("connected") self._logger.info("connected")
@ -325,11 +358,8 @@ class Client(Base):
def _greet(self): def _greet(self):
greeting = self._sock_read.readline() greeting = self._read_line()
self._logger.debug("greeting: %s", greeting.strip()) self._logger.debug("greeting: %s", greeting.strip())
if not greeting.endswith("\n"):
self._disconnect_socket()
raise ConnectionException("incomplete line")
if not greeting.startswith(Client.PROTOCOL_GREETING): if not greeting.startswith(Client.PROTOCOL_GREETING):
self._disconnect_socket() self._disconnect_socket()
raise ProtocolException("invalid greeting: {}".format(greeting)) raise ProtocolException("invalid greeting: {}".format(greeting))
@ -343,9 +373,6 @@ class Client(Base):
def _disconnect_socket(self): def _disconnect_socket(self):
if self._sock_read is not None:
self._sock_read.close()
self._sock_read = None
if self._sock_write is not None: if self._sock_write is not None:
self._sock_write.close() self._sock_write.close()
self._sock_write = None self._sock_write = None
@ -365,8 +392,8 @@ class Client(Base):
self._logger.info("idle subsystems: %r", subsystems) self._logger.info("idle subsystems: %r", subsystems)
if subsystems: if subsystems:
if subsystems['changed'] == 'player': if subsystems['changed'] == 'player':
self.get_status()
self.load_playlist() self.load_playlist()
self.get_status()
if subsystems['changed'] == 'mixer': if subsystems['changed'] == 'mixer':
self.get_status() self.get_status()
if subsystems['changed'] == 'playlist': if subsystems['changed'] == 'playlist':
@ -406,7 +433,7 @@ class Client(Base):
if 'time' in status: if 'time' in status:
time = int(status['time'].split(':')[0]) time = int(status['time'].split(':')[0])
# Volume # Volume
volume = 0 volume = -1
if 'volume' in status: if 'volume' in status:
volume = int(status['volume']) volume = int(status['volume'])
# Error # Error
@ -442,7 +469,7 @@ class Client(Base):
bitrate = None bitrate = None
if 'bitrate' in status: if 'bitrate' in status:
bitrate = status['bitrate'] bitrate = status['bitrate']
self._callback(Client.SIGNAL_STATUS, state, album, pos, time, volume, file, audio, bitrate, error) return (state, album, pos, time, volume, file, audio, bitrate, error)
def _get_stats(self): def _get_stats(self):
@ -475,7 +502,7 @@ class Client(Base):
uptime = 0 uptime = 0
if 'uptime' in stats: if 'uptime' in stats:
uptime = stats['uptime'] uptime = stats['uptime']
self._callback(Client.SIGNAL_STATS, artists, albums, songs, dbplaytime, playtime, uptime) return (artists, albums, songs, dbplaytime, playtime, uptime)
def _get_output_devices(self): def _get_output_devices(self):
@ -485,7 +512,7 @@ class Client(Base):
device = OutputDevice(output['outputid'], output['outputname']) device = OutputDevice(output['outputid'], output['outputname'])
device.set_enabled(int(output['outputenabled']) == 1) device.set_enabled(int(output['outputenabled']) == 1)
devices.append(device) devices.append(device)
self._callback(Client.SIGNAL_LOAD_OUTPUT_DEVICES, devices) return (devices, )
def _enable_output_device(self, device, enabled): def _enable_output_device(self, device, enabled):
@ -498,9 +525,12 @@ class Client(Base):
def _load_albums(self): def _load_albums(self):
"""Action: Perform the real update.""" """Action: Perform the real update."""
self._callback(Client.SIGNAL_INIT_ALBUMS)
self._albums = {} self._albums = {}
# Albums # Albums
for album in self._parse_list(self._call('list album'), ['album']): for album in self._parse_list(self._call('list album'), ['album']):
self._callback(Client.SIGNAL_PULSE_ALBUMS)
# Album # Album
album = self._extract_album(album) album = self._extract_album(album)
self._logger.debug("album: %r", album) self._logger.debug("album: %r", album)
@ -510,7 +540,7 @@ class Client(Base):
if track: if track:
self._logger.debug("track: %r", track) self._logger.debug("track: %r", track)
album.add_track(track) album.add_track(track)
self._callback(Client.SIGNAL_LOAD_ALBUMS, self._albums) return (self._albums, )
def _update(self): def _update(self):
@ -533,7 +563,7 @@ class Client(Base):
self._logger.debug("album: %r", album) self._logger.debug("album: %r", album)
if track: if track:
album.add_track(track) album.add_track(track)
self._callback(Client.SIGNAL_LOAD_PLAYLIST, self._playlist) return (self._playlist, )
def _clear_playlist(self): def _clear_playlist(self):
@ -612,8 +642,26 @@ class Client(Base):
self._call('setvol', volume) self._call('setvol', volume)
def _get_custom(self, name): def _get_albumart(self, album):
self._callback(Client.SIGNAL_CUSTOM, name) if album in self._albums:
album = self._albums[album]
self._logger.debug("get albumart for album \"%s\"", album.get_title())
# Use "albumart" command
if album.get_tracks():
try:
return (album, self._read_binary('albumart', album.get_tracks()[0].get_file(), False))
except CommandException as e:
# The "albumart" command throws an exception if not found
if e.get_error_number() != Client.PROTOCOL_ERROR_NOEXISTS:
raise e
# If no albumart can be found, use "readpicture" command
for track in album.get_tracks():
data = self._read_binary('readpicture', track.get_file(), True)
if data:
return (album, data)
return (album, None)
def _start_worker(self): def _start_worker(self):
@ -640,22 +688,46 @@ class Client(Base):
def _add_action(self, method, *args): def _add_action(self, method, *args):
"""Add an action to the action list.""" """Add an action to the action list."""
self._logger.debug("add action %r (%r)", method.__name__, args) self._logger.debug("add action %r (%r)", method.__name__, args)
action = (method, args) future = concurrent.futures.Future()
action = (future, method, args)
self._actions.put(action)
self._noidle()
return future
def _add_action_signal(self, signal, method, *args):
"""Add an action to the action list that triggers a callback."""
self._logger.debug("add action signal %r: %r (%r)", signal, method.__name__, args)
future = Future(signal)
future.add_done_callback(self._callback_future)
self._add_action_future(future, method, *args)
return future
def _add_action_future(self, future, method, *args):
"""Add an action to the action list based on a futre."""
self._logger.debug("add action future %r (%r)", method.__name__, args)
action = (future, method, args)
self._actions.put(action) self._actions.put(action)
self._noidle() self._noidle()
def _work(self, action): def _work(self, action):
(method, args) = action (future, method, args) = action
self._logger.debug("work: %r", method.__name__) self._logger.debug("work: %r", method.__name__)
try: try:
method(*args) result = method(*args)
future.set_result(result)
except ConnectionException as e: except ConnectionException as e:
self._logger.exception(e) self._logger.exception(e)
future.set_exception(e)
self._callback(Client.SIGNAL_ERROR, e) self._callback(Client.SIGNAL_ERROR, e)
self._disconnect_socket() self._disconnect_socket()
except Exception as e: except Exception as e:
self._logger.exception(e) self._logger.exception(e)
future.set_exception(e)
self._callback(Client.SIGNAL_ERROR, e) self._callback(Client.SIGNAL_ERROR, e)
@ -664,7 +736,7 @@ class Client(Base):
self._write(command, args) self._write(command, args)
return self._read() return self._read()
except MPDException as e: except MPDException as e:
if command == 'idle' and e.get_error() == Client.PROTOCOL_ERROR_PERMISSION: if command == 'idle' and e.get_error_number() == Client.PROTOCOL_ERROR_PERMISSION:
self.disconnect() self.disconnect()
self._callback(Client.SIGNAL_ERROR, e) self._callback(Client.SIGNAL_ERROR, e)
@ -673,7 +745,7 @@ class Client(Base):
try: try:
self._write(command, args) self._write(command, args)
except MPDException as e: except MPDException as e:
if command == 'idle' and e.get_error() == Client.PROTOCOL_ERROR_PERMISSION: if command == 'idle' and e.get_error_number() == Client.PROTOCOL_ERROR_PERMISSION:
self.disconnect() self.disconnect()
self._callback(Client.SIGNAL_ERROR, e) self._callback(Client.SIGNAL_ERROR, e)
@ -691,16 +763,10 @@ class Client(Base):
def _read(self): def _read(self):
self._logger.debug("reading response") self._logger.debug("reading response")
response = [] response = []
line = self._sock_read.readline() line = self._read_line()
if not line.endswith("\n"):
self._disconnect_socket()
raise ConnectionException("incomplete line")
while not line.startswith(Client.PROTOCOL_COMPLETION) and not line.startswith(Client.PROTOCOL_ERROR): while not line.startswith(Client.PROTOCOL_COMPLETION) and not line.startswith(Client.PROTOCOL_ERROR):
response.append(line.strip()) response.append(line.strip())
line = self._sock_read.readline() line = self._read_line()
if not line.endswith("\n"):
self._disconnect_socket()
raise ConnectionException("incomplete line")
if line.startswith(Client.PROTOCOL_COMPLETION): if line.startswith(Client.PROTOCOL_COMPLETION):
self._logger.debug("response complete") self._logger.debug("response complete")
if line.startswith(Client.PROTOCOL_ERROR): if line.startswith(Client.PROTOCOL_ERROR):
@ -711,6 +777,115 @@ class Client(Base):
return response return response
def _read_line(self):
self._logger.debug("reading line")
# Read from the buffer
data = self._buffer_get_char(b'\x0A')
if not data.endswith(b'\x0A'):
# Read more from socket until next line break
while b'\x0A' not in data:
buf = self._sock.recv(Client.SOCKET_BUFSIZE)
if buf:
data += buf
else:
break
# Get first line from data, add rest to buffer
if data:
lines = data.split(b'\x0A', 1)
data = lines[0]
self._buffer_set(lines[1])
if data:
return data.decode('utf-8')
return None
def _read_binary(self, command, filename, has_mimetype):
data = None
size = 1
offset = 0
index = 0
# Read data until size is reached
while offset < size:
self._write(command, args=[filename, offset])
# Read first line
line = self._read_line()
# Check first line for error
if line.startswith(Client.PROTOCOL_ERROR):
error = line[len(Client.PROTOCOL_ERROR):].strip()
self._logger.debug("command failed: %r", error)
raise CommandException(error)
# Check first line for completion
if line.startswith(Client.PROTOCOL_COMPLETION):
break
# First line is the file size
size = int(self._parse_dict([line])['size'])
self._logger.debug("size: %d", size)
# For some commands the second line is the mimetype
if has_mimetype:
mimetype = self._parse_dict([self._read_line()])['type']
# Next line is the count of bytes read
binary = int(self._parse_dict([self._read_line()])['binary'])
self._logger.debug("binary: %d", binary)
# Create new data array on the first iteration
if not data:
data = bytearray(size)
# Create a view for the current chunk of data
data_view = memoryview(data)[offset:offset+binary]
# Read actual bytes
self._read_bytes(data_view, binary)
offset += binary
# Read line break to complete previous repsonse
self._read_line()
# Read command completion
end = self._read_line()
if not end.startswith(Client.PROTOCOL_COMPLETION):
self._logger.debug("albumart not completed")
data = None
break
return data
def _read_bytes(self, buf, nbytes):
self._logger.debug("reading bytes")
# Use already buffered data
buf_read = self._buffer_get_size(nbytes)
nbytes_read = len(buf_read)
buf[0:nbytes_read] = buf_read
# Read additional data from socket
nbytes = nbytes - nbytes_read
if nbytes > 0:
buf_view = memoryview(buf)[nbytes_read:]
nbytes_read += self._sock.recv_into(buf_view, nbytes)
return nbytes_read
def _buffer_get_char(self, char):
pos = self._buffer.find(char)
if pos < 0:
pos = len(self._buffer)-1
buf = self._buffer[0:pos+1]
self._buffer = self._buffer[pos+1:]
return buf
def _buffer_get_size(self, size):
buf = self._buffer[0:size]
self._logger.debug("get %d bytes from buffer", len(buf))
self._buffer = self._buffer[size:]
self._logger.debug("leaving %d in the buffer", len(self._buffer))
return buf
def _buffer_set(self, buf):
self._logger.debug("set %d %s as buffer", len(buf), type(buf))
self._buffer = buf
def _parse_dict(self, response): def _parse_dict(self, response):
dict = {} dict = {}
if response: if response:
@ -751,7 +926,7 @@ class Client(Base):
if lookup and id in self._albums.keys(): if lookup and id in self._albums.keys():
album = self._albums[id] album = self._albums[id]
else: else:
album = MCGAlbum(song['album'], self._host, self._image_dir) album = MCGAlbum(song['album'], self._host)
if lookup: if lookup:
self._albums[id] = album self._albums[id] = album
return album return album
@ -769,6 +944,8 @@ class Client(Base):
track.set_date(song['date']) track.set_date(song['date'])
if 'albumartist' in song: if 'albumartist' in song:
track.set_albumartists(song['albumartist']) track.set_albumartists(song['albumartist'])
if 'last-modified' in song:
track.set_last_modified(song['last-modified'])
return track return track
@ -819,7 +996,7 @@ class MCGAlbum:
_FILTER_DELIMITER = ' ' _FILTER_DELIMITER = ' '
def __init__(self, title, host, image_dir): def __init__(self, title, host):
self._artists = [] self._artists = []
self._albumartists = [] self._albumartists = []
self._pathes = [] self._pathes = []
@ -828,11 +1005,9 @@ class MCGAlbum:
self._title = title self._title = title
self._dates = [] self._dates = []
self._host = host self._host = host
self._image_dir = image_dir
self._tracks = [] self._tracks = []
self._length = 0 self._length = 0
self._cover = None self._last_modified = None
self._cover_searched = False
self._id = Utils.generate_id(title) self._id = Utils.generate_id(title)
@ -892,6 +1067,9 @@ class MCGAlbum:
path = os.path.dirname(track.get_file()) path = os.path.dirname(track.get_file())
if path not in self._pathes: if path not in self._pathes:
self._pathes.append(path) self._pathes.append(path)
if track.get_last_modified():
if not self._last_modified or track.get_last_modified() > self._last_modified:
self._last_modified = track.get_last_modified()
def get_tracks(self): def get_tracks(self):
@ -902,10 +1080,8 @@ class MCGAlbum:
return self._length return self._length
def get_cover(self): def get_last_modified(self):
if self._cover is None and not self._cover_searched: return self._last_modified
self._find_cover()
return self._cover
def filter(self, filter_string): def filter(self, filter_string):
@ -934,7 +1110,7 @@ class MCGAlbum:
return True return True
def compare(album1, album2, criterion=None): def compare(album1, album2, criterion=None, reverse=False):
if criterion == None: if criterion == None:
criterion = SortOrder.TITLE criterion = SortOrder.TITLE
if criterion == SortOrder.ARTIST: if criterion == SortOrder.ARTIST:
@ -943,70 +1119,25 @@ class MCGAlbum:
value_function = "get_title" value_function = "get_title"
elif criterion == SortOrder.YEAR: elif criterion == SortOrder.YEAR:
value_function = "get_date" value_function = "get_date"
elif criterion == SortOrder.MODIFIED:
value_function = "get_last_modified"
reverseMultiplier = -1 if reverse else 1
value1 = getattr(album1, value_function)() value1 = getattr(album1, value_function)()
value2 = getattr(album2, value_function)() value2 = getattr(album2, value_function)()
if value1 is None and value2 is None: if value1 is None and value2 is None:
return 0 return 0
elif value1 is None: elif value1 is None:
return -1 return -1 * reverseMultiplier
elif value2 is None: elif value2 is None:
return 1 return 1 * reverseMultiplier
if value1 < value2: if value1 < value2:
return -1 return -1 * reverseMultiplier
elif value1 == value2: elif value1 == value2:
return 0 return 0
else: else:
return 1 return 1 * reverseMultiplier
def _find_cover(self):
names = list(MCGAlbum._FILE_NAMES)
names.append(self._title)
if self._host == "localhost" or self._host == "127.0.0.1" or self._host == "::1":
self._cover = self._find_cover_local(names)
else:
self._cover = self._find_cover_web(names)
self._cover_searched = True
def _find_cover_web(self, names):
for path in self._pathes:
for name in names:
for ext in self._FILE_EXTS:
url = '/'.join([
'http:/',
self._host,
urllib.request.quote(self._image_dir.strip("/")),
urllib.request.quote(path),
urllib.request.quote('.'.join([name, ext]))
])
request = urllib.request.Request(url)
try:
response = urllib.request.urlopen(request)
return url
except urllib.error.URLError as e:
pass
def _find_cover_local(self, names):
for path in self._pathes:
for name in names:
for ext in self._FILE_EXTS:
filename = os.path.join(self._image_dir, path, '.'.join([name, ext]))
if os.path.isfile(filename):
return filename
return self._find_cover_local_fallback()
def _find_cover_local_fallback(self):
for path in self._pathes:
for ext in self._FILE_EXTS:
filename = os.path.join(self._image_dir, path, "*."+ext)
files = glob.glob(filename)
if len(files) > 0:
return files[0]
@ -1027,6 +1158,7 @@ class MCGTrack:
self._track = None self._track = None
self._length = 0 self._length = 0
self._date = None self._date = None
self._last_modified = None
def __eq__(self, other): def __eq__(self, other):
@ -1098,6 +1230,18 @@ class MCGTrack:
return self._file return self._file
def set_last_modified(self, date_string):
if date_string:
try:
self._last_modified = dateutil.parser.isoparse(date_string)
except ValueError as e:
self._logger.debug("Invalid date format: %s", date_string)
def get_last_modified(self):
return self._last_modified
class MCGPlaylistTrack(MCGTrack): class MCGPlaylistTrack(MCGTrack):
@ -1161,6 +1305,7 @@ class MCGCache():
def __init__(self, host, size): def __init__(self, host, size):
self._logger = logging.getLogger(__name__)
self._host = host self._host = host
self._size = size self._size = size
self._dirname = os.path.expanduser(os.path.join(MCGCache.DIRNAME, host)) self._dirname = os.path.expanduser(os.path.join(MCGCache.DIRNAME, host))
@ -1180,7 +1325,11 @@ class MCGCache():
filename = os.path.join(self._dirname, MCGCache.SIZE_FILENAME) filename = os.path.join(self._dirname, MCGCache.SIZE_FILENAME)
if os.path.exists(filename): if os.path.exists(filename):
with open(filename, 'r') as f: with open(filename, 'r') as f:
try:
size = int(f.readline()) size = int(f.readline())
except:
self._logger.warning("invalid cache file: %s, deleting file", filename, exc_info=True)
size = None
# Clear cache if size has changed # Clear cache if size has changed
if size != self._size: if size != self._size:
self._clear() self._clear()

99
src/connectionpanel.py Normal file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
import locale
from gi.repository import Gtk, Gio, GObject, Adw
from mcg.zeroconf import ZeroconfProvider
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/connection-panel.ui')
class ConnectionPanel(Adw.Bin):
__gtype_name__ = 'McgConnectionPanel'
__gsignals__ = {
'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str))
}
# Widgets
toolbar = Gtk.Template.Child()
zeroconf_list = Gtk.Template.Child()
host_row = Gtk.Template.Child()
port_spinner = Gtk.Template.Child()
password_row = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Zeroconf provider
self._zeroconf_provider = ZeroconfProvider()
self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service)
def on_new_service(self, service):
name, host, port = service
row_button = Gtk.Button()
row_button.set_label(locale.gettext("use"))
row_button.connect("clicked", self.on_service_selected, host, port)
row = Adw.ActionRow()
row.set_title(name)
row.set_subtitle("{} ({})".format(host, port))
row.add_suffix(row_button)
self.zeroconf_list.insert(row, -1)
def on_service_selected(self, widget, host, port):
self.set_host(host)
self.set_port(port)
@Gtk.Template.Callback()
def on_host_entry_apply(self, widget):
self._call_back()
@Gtk.Template.Callback()
def on_port_spinner_value_changed(self, widget):
self._call_back()
def set_host(self, host):
self.host_row.set_text(host)
def get_host(self):
return self.host_row.get_text()
def set_port(self, port):
self.port_spinner.set_value(port)
def get_port(self):
return self.port_spinner.get_value_as_int()
def set_password(self, password):
if password is None:
password = ""
self.password_row.set_text(password)
def get_password(self):
if self.password_row.get_text() == "":
return None
else:
return self.password_entry.get_text()
def _call_back(self):
self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(),)

262
src/coverpanel.py Normal file
View file

@ -0,0 +1,262 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
import logging
import math
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf
from mcg.utils import Utils
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/cover-panel.ui')
class CoverPanel(Gtk.Overlay):
__gtype_name__ = 'McgCoverPanel'
__gsignals__ = {
'toggle-fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()),
'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,)),
'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,))
}
# Widgets
# Toolbar
toolbar = Gtk.Template.Child()
fullscreen_button = Gtk.Template.Child()
# Cover
cover_stack = Gtk.Template.Child()
cover_spinner = Gtk.Template.Child()
cover_default = Gtk.Template.Child()
cover_scroll = Gtk.Template.Child()
cover_box = Gtk.Template.Child()
cover_image = Gtk.Template.Child()
# Album Infos
cover_info_scroll = Gtk.Template.Child()
info_revealer = Gtk.Template.Child()
album_title_label = Gtk.Template.Child()
album_date_label = Gtk.Template.Child()
album_artist_label = Gtk.Template.Child()
# Songs
songs_scale = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._current_album = None
self._current_cover_album = None
self._cover_pixbuf = None
self._timer = None
self._properties = {}
self._icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
self._fullscreened = False
self._current_size = None
# Initial actions
GObject.idle_add(self._enable_tracklist)
# Click handler for image
clickController = Gtk.GestureClick()
clickController.connect('pressed', self.on_cover_box_pressed)
self.cover_box.add_controller(clickController)
# Button controller for songs scale
buttonController = Gtk.GestureClick()
buttonController.connect('pressed', self.on_songs_scale_pressed)
buttonController.connect('unpaired-release', self.on_songs_scale_released)
self.songs_scale.add_controller(buttonController)
def get_toolbar(self):
return self.toolbar
def set_selected(self, selected):
pass
def on_cover_box_pressed(self, widget, npress, x, y):
if self._current_album and npress == 2:
self.emit('toggle-fullscreen')
def set_width(self, width):
GObject.idle_add(self._resize_image)
self.cover_info_scroll.set_max_content_width(width // 2)
def on_songs_scale_pressed(self, widget, npress, x, y):
if self._timer:
GObject.source_remove(self._timer)
self._timer = None
def on_songs_scale_released(self, widget, x, y, npress, sequence):
value = int(self.songs_scale.get_value())
time = self._current_album.get_length()
tracks = self._current_album.get_tracks()
pos = 0
for index in range(len(tracks)-1, -1, -1):
time = time - tracks[index].get_length()
pos = tracks[index].get_pos()
if time < value:
break
time = max(value - time - 1, 0)
self.emit('set-song', pos, time)
def set_album(self, album):
if album:
# Set labels
self.album_title_label.set_label(album.get_title())
self.album_date_label.set_label(', '.join(album.get_dates()))
self.album_artist_label.set_label(', '.join(album.get_albumartists()))
# Set tracks
self._set_tracks(album)
# Load cover
if album != self._current_cover_album:
self.cover_stack.set_visible_child(self.cover_spinner)
self.cover_spinner.start()
self.emit('albumart', album.get_id() if album else None)
# Set current album
self._current_album = album
self._enable_tracklist()
self.fullscreen_button.set_sensitive(self._current_album is not None)
def set_play(self, pos, time):
if self._timer is not None:
GObject.source_remove(self._timer)
self._timer = None
tracks = self._current_album.get_tracks()
for index in range(0, pos):
time = time + tracks[index].get_length()
self.songs_scale.set_value(time+1)
self._timer = GObject.timeout_add(1000, self._playing)
def set_pause(self):
if self._timer is not None:
GObject.source_remove(self._timer)
self._timer = None
def set_fullscreen(self, active):
if active:
self.info_revealer.set_reveal_child(False)
GObject.idle_add(self._resize_image)
self._fullscreened = True
else:
self._fullscreened = False
if self._current_album:
self.info_revealer.set_reveal_child(True)
GObject.idle_add(self._resize_image)
def set_albumart(self, album, data):
if album == self._current_album:
if data:
# Load image and draw it
try:
self._cover_pixbuf = Utils.load_pixbuf(data)
except Exception as e:
self._logger.exception("Failed to set albumart")
self._cover_pixbuf = None
else:
# Reset image
self._cover_pixbuf = None
self._current_size = None
self._current_cover_album = album
# Show image
GObject.idle_add(self._show_image)
def _set_tracks(self, album):
self.songs_scale.clear_marks()
self.songs_scale.set_range(0, album.get_length())
length = 0
for track in album.get_tracks():
cur_length = length
if length > 0 and length < album.get_length():
cur_length = cur_length + 1
self.songs_scale.add_mark(
cur_length,
Gtk.PositionType.RIGHT,
GObject.markup_escape_text(
Utils.create_track_title(track)
)
)
length = length + track.get_length()
self.songs_scale.add_mark(
length,
Gtk.PositionType.RIGHT,
"{0[0]:02d}:{0[1]:02d} minutes".format(divmod(length, 60))
)
def _enable_tracklist(self):
if self._current_album:
# enable
if not self._fullscreened:
self.info_revealer.set_reveal_child(True)
else:
# disable
self.info_revealer.set_reveal_child(False)
def _playing(self):
value = self.songs_scale.get_value() + 1
self.songs_scale.set_value(value)
return True
def _show_image(self):
if self._cover_pixbuf:
self._resize_image()
self.cover_stack.set_visible_child(self.cover_scroll)
else:
self.cover_stack.set_visible_child(self.cover_default)
self.cover_spinner.stop()
def _resize_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
# Get size
size_width = self.cover_stack.get_size(Gtk.Orientation.HORIZONTAL)
size_height = self.cover_stack.get_size(Gtk.Orientation.HORIZONTAL)
# Abort if size is the same
if self._current_size:
current_width, current_height = self._current_size
if size_width == current_width and size_height == current_height:
return
self._current_size = (size_width, size_height,)
# Get pixelbuffer
pixbuf = self._cover_pixbuf
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size_width) / float(pixbuf.get_width())
ratioH = float(size_height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
self.cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self.cover_image.show()

501
src/librarypanel.py Normal file
View file

@ -0,0 +1,501 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
import locale
import logging
import math
import threading
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, Gio, Adw
from mcg import client
from mcg.albumheaderbar import AlbumHeaderbar
from mcg.utils import SortOrder
from mcg.utils import Utils
from mcg.utils import GridItem
from mcg.utils import SearchFilter
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/library-panel.ui')
class LibraryPanel(Adw.Bin):
__gtype_name__ = 'McgLibraryPanel'
__gsignals__ = {
'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()),
'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()),
'update': (GObject.SIGNAL_RUN_FIRST, None, ()),
'play': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
'queue': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
'queue-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'item-size-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'sort-order-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'sort-type-changed': (GObject.SIGNAL_RUN_FIRST, None, (bool,)),
'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
}
# Widgets
library_stack = Gtk.Template.Child()
panel_normal = Gtk.Template.Child()
panel_standalone = Gtk.Template.Child()
actionbar_revealer = Gtk.Template.Child()
# Toolbar
toolbar = Gtk.Template.Child()
select_button = Gtk.Template.Child()
toolbar_search_bar = Gtk.Template.Child()
toolbar_popover = Gtk.Template.Child()
toolbar_sort_order_button = Gtk.Template.Child()
sort_artist = Gtk.Template.Child()
sort_title = Gtk.Template.Child()
sort_year = Gtk.Template.Child()
sort_modified = Gtk.Template.Child()
grid_scale = Gtk.Template.Child()
# Filter/search bar
filter_bar = Gtk.Template.Child()
filter_entry = Gtk.Template.Child()
# Progress Bar
stack = Gtk.Template.Child()
progress_box = Gtk.Template.Child()
progress_image = Gtk.Template.Child()
progress_bar = Gtk.Template.Child()
scroll = Gtk.Template.Child()
# Library Grid
library_grid = Gtk.Template.Child()
# Action bar (normal)
actionbar = Gtk.Template.Child()
actionbar_standalone = Gtk.Template.Child()
# Standalone Image
standalone_stack = Gtk.Template.Child()
standalone_spinner = Gtk.Template.Child()
standalone_scroll = Gtk.Template.Child()
standalone_image = Gtk.Template.Child()
def __init__(self, client, **kwargs):
super().__init__(**kwargs)
self._logger = logging.getLogger(__name__)
self._client = client
self._buttons = {}
self._albums = None
self._host = "localhost"
self._item_size = 150
self._sort_order = SortOrder.YEAR
self._sort_type = Gtk.SortType.DESCENDING
self._grid_pixbufs = {}
self._grid_width = 0
self._old_ranges = {}
self._library_lock = threading.Lock()
self._library_stop = threading.Event()
self._icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
self._standalone_pixbuf = None
self._selected_albums = []
self._is_selected = False
# Widgets
# Header bar
self._headerbar_standalone = AlbumHeaderbar()
self._headerbar_standalone.connect('close', self.on_standalone_close_clicked)
# Library Grid: Model
self._library_grid_model = Gio.ListStore()
self._library_grid_filter = Gtk.FilterListModel()
self._library_grid_filter.set_model(self._library_grid_model)
self._library_grid_selection_multi = Gtk.MultiSelection.new(self._library_grid_filter)
self._library_grid_selection_single = Gtk.SingleSelection.new(self._library_grid_filter)
# Library Grid
self.library_grid.set_model(self._library_grid_selection_single)
# Toolbar menu
self.grid_scale.set_value(self._item_size)
self._toolbar_sort_buttons = {
SortOrder.ARTIST: self.sort_artist,
SortOrder.TITLE: self.sort_title,
SortOrder.YEAR: self.sort_year,
SortOrder.MODIFIED: self.sort_modified
}
# Button controller for grid scale
buttonController = Gtk.GestureClick()
buttonController.connect('unpaired-release', self.on_grid_scale_released)
self.grid_scale.add_controller(buttonController)
def get_headerbar_standalone(self):
return self._headerbar_standalone
def get_toolbar(self):
return self.toolbar
def set_selected(self, selected):
self._is_selected = selected
@Gtk.Template.Callback()
def on_select_toggled(self, widget):
if self.select_button.get_active():
self.actionbar_revealer.set_reveal_child(True)
self.library_grid.set_model(self._library_grid_selection_multi)
self.library_grid.set_single_click_activate(False)
self.library_grid.get_style_context().add_class(Utils.CSS_SELECTION)
else:
self.actionbar_revealer.set_reveal_child(False)
self.library_grid.set_model(self._library_grid_selection_single)
self.library_grid.set_single_click_activate(True)
self.library_grid.get_style_context().remove_class(Utils.CSS_SELECTION)
@Gtk.Template.Callback()
def on_update_clicked(self, widget):
self.emit('update')
def on_grid_scale_released(self, widget, x, y, npress, sequence):
size = math.floor(self.grid_scale.get_value())
range = self.grid_scale.get_adjustment()
if size < range.get_lower() or size > range.get_upper():
return
self._item_size = size
self.emit('item-size-changed', size)
self._redraw()
GObject.idle_add(self.toolbar_popover.popdown)
@Gtk.Template.Callback()
def on_grid_scale_changed(self, widget):
size = math.floor(self.grid_scale.get_value())
range = widget.get_adjustment()
if size < range.get_lower() or size > range.get_upper():
return
self._set_widget_grid_size(self.library_grid, size, True)
@Gtk.Template.Callback()
def on_sort_toggled(self, widget):
if widget.get_active():
self._sort_order = [key for key, value in self._toolbar_sort_buttons.items() if value is widget][0]
self._sort_grid_model()
self.emit('sort-order-changed', self._sort_order)
@Gtk.Template.Callback()
def on_sort_order_toggled(self, button):
if button.get_active():
self._sort_type = Gtk.SortType.DESCENDING
else:
self._sort_type = Gtk.SortType.ASCENDING
self._sort_grid_model()
self.emit('sort-type-changed', button.get_active())
def set_size(self, width, height):
self._set_marks()
self._resize_standalone_image()
@Gtk.Template.Callback()
def on_filter_entry_changed(self, widget):
self._library_grid_filter.set_filter(SearchFilter(self.filter_entry.get_text()))
@Gtk.Template.Callback()
def on_library_grid_clicked(self, widget, position):
# Get selected album
item = self._library_grid_filter.get_item(position)
album = item.get_album()
id = album.get_id()
self._selected_albums = [album]
self.emit('albumart', id)
# Show standalone album
if widget.get_model() == self._library_grid_selection_single:
# Set labels
self._headerbar_standalone.set_album(album)
# Show panel
self._open_standalone()
# Set cover loading indicator
self.standalone_stack.set_visible_child(self.standalone_spinner)
self.standalone_spinner.start()
@Gtk.Template.Callback()
def on_selection_cancel_clicked(self, widget):
self.select_button.set_active(False)
@Gtk.Template.Callback()
def on_selection_add_clicked(self, widget):
self.emit('queue-multiple', self._get_selected_albums())
self.select_button.set_active(False)
@Gtk.Template.Callback()
def on_standalone_play_clicked(self, widget):
self.emit('play', self._selected_albums[0].get_id())
self._close_standalone()
@Gtk.Template.Callback()
def on_standalone_queue_clicked(self, widget):
self.emit('queue', self._selected_albums[0].get_id())
self._close_standalone()
def on_standalone_close_clicked(self, widget):
self._close_standalone()
def show_search(self):
self.filter_bar.set_search_mode(True)
def set_item_size(self, item_size):
if self._item_size != item_size:
self._item_size = item_size
self.grid_scale.set_value(item_size)
self._redraw()
def get_item_size(self):
return self._item_size
def set_sort_order(self, sort):
button = self._toolbar_sort_buttons[sort]
if button:
self._sort_order = [key for key, value in self._toolbar_sort_buttons.items() if value is button][0]
if not button.get_active():
button.set_active(True)
self._sort_grid_model()
def set_sort_type(self, sort_type):
sort_type_gtk = Gtk.SortType.DESCENDING if sort_type else Gtk.SortType.ASCENDING
if sort_type_gtk != self._sort_type:
self._sort_type = sort_type_gtk
self.toolbar_sort_order_button.set_active(sort_type)
self._sort_grid_model()
def get_sort_type(self):
return (self._sort_type != Gtk.SortType.ASCENDING)
def init_albums(self):
self.progress_bar.set_text(locale.gettext("Loading albums"))
def load_albums(self):
self.progress_bar.pulse()
def set_albums(self, host, albums):
self._host = host
self._library_stop.set()
threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start()
def set_albumart(self, album, data):
if album in self._selected_albums:
if data:
# Load image and draw it
try:
self._standalone_pixbuf = Utils.load_pixbuf(data)
except Exception as e:
self._logger.exception("Failed to set albumart")
self._standalone_pixbuf = self._get_default_image()
else:
self._standalone_pixbuf = self._get_default_image()
# Show image
GObject.idle_add(self._show_image)
def _sort_grid_model(self):
GObject.idle_add(self._library_grid_model.sort, self._grid_model_compare_func, self._sort_order, self._sort_type)
def _grid_model_compare_func(self, item1, item2, criterion, order):
return client.MCGAlbum.compare(item1.get_album(), item2.get_album(), criterion, (order == Gtk.SortType.DESCENDING))
def stop_threads(self):
self._library_stop.set()
def _set_albums(self, host, albums, size):
self._library_lock.acquire()
self._albums = albums
stack_transition_type = self.stack.get_transition_type()
GObject.idle_add(self.stack.set_transition_type, Gtk.StackTransitionType.NONE)
GObject.idle_add(self.stack.set_visible_child, self.progress_box)
GObject.idle_add(self.progress_bar.set_fraction, 0.0)
GObject.idle_add(self.stack.set_transition_type, stack_transition_type)
GObject.idle_add(self._library_grid_model.remove_all)
i = 0
n = len(albums)
cache = client.MCGCache(host, size)
self._grid_pixbufs.clear()
for album_id in albums.keys():
album = albums[album_id]
pixbuf = None
try:
pixbuf = Utils.load_thumbnail(cache, self._client, album, size)
except client.CommandException:
# Exception is handled by client
pass
except Exception as e:
self._logger.exception("Failed to load albumart", e)
if pixbuf is None:
pixbuf = self._icon_theme.lookup_icon(
Utils.STOCK_ICON_DEFAULT,
None,
self._item_size,
self._item_size,
Gtk.TextDirection.LTR,
Gtk.IconLookupFlags.FORCE_SYMBOLIC
)
if pixbuf is not None:
self._grid_pixbufs[album.get_id()] = pixbuf
GObject.idle_add(self._library_grid_model.append, GridItem(album, pixbuf))
i += 1
GObject.idle_add(self.progress_bar.set_fraction, i/n)
GObject.idle_add(self.progress_bar.set_text, locale.gettext("Loading images"))
self._library_lock.release()
GObject.idle_add(self.stack.set_visible_child, self.scroll)
self._sort_grid_model()
def _set_widget_grid_size(self, grid_widget, size, vertical):
self._library_stop.set()
threading.Thread(target=self._set_widget_grid_size_thread, args=(grid_widget, size, vertical,)).start()
def _set_widget_grid_size_thread(self, grid_widget, size, vertical):
self._library_lock.acquire()
self._library_stop.clear()
if size == self._item_size:
self._library_lock.release()
return
for i in range(self._library_grid_model.get_n_items()):
grid_item = self._library_grid_model.get_item(i)
album_id = grid_item.get_album().get_id()
pixbuf = self._grid_pixbufs[album_id]
if pixbuf is not None:
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
else:
pixbuf = self._icon_theme.lookup_icon(
Utils.STOCK_ICON_DEFAULT,
None,
size,
size,
Gtk.TextDirection.LTR,
Gtk.IconLookupFlags.FORCE_SYMBOLIC
)
GObject.idle_add(grid_item.set_cover, pixbuf)
if self._library_stop.is_set():
self._library_lock.release()
return
self._library_lock.release()
def _show_image(self):
self._resize_standalone_image()
self.standalone_stack.set_visible_child(self.standalone_scroll)
self.standalone_spinner.stop()
def _redraw(self):
if self._albums is not None:
self.set_albums(self._host, self._albums)
def _set_marks(self):
width = self.scroll.get_width()
if width == self._grid_width:
return
self._grid_width = width
self.grid_scale.clear_marks()
lower = int(self.grid_scale.get_adjustment().get_lower())
upper = int(self.grid_scale.get_adjustment().get_upper())
countMin = max(int(width / upper), 1)
countMax = max(int(width / lower), 1)
for index in range(countMin, countMax):
pixel = int(width / index)
pixel = pixel - (2 * int(pixel / 100))
self.grid_scale.add_mark(
pixel,
Gtk.PositionType.BOTTOM,
None
)
def _open_standalone(self):
self.library_stack.set_visible_child(self.panel_standalone)
self.emit('open-standalone')
def _close_standalone(self):
self.library_stack.set_visible_child(self.panel_normal)
self.emit('close-standalone')
def _resize_standalone_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
# Get size
size_width = self.standalone_stack.get_width()
size_height = self.standalone_stack.get_height()
# Get pixelbuffer
pixbuf = self._standalone_pixbuf
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size_width) / float(pixbuf.get_width())
ratioH = float(size_height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self.standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self.standalone_image.show()
def _get_default_image(self):
return self._icon_theme.lookup_icon(
Utils.STOCK_ICON_DEFAULT,
None,
512,
512,
Gtk.TextDirection.LTR,
Gtk.IconLookupFlags.FORCE_SYMBOLIC
)
def _get_selected_albums(self):
albums = []
for i in range(self.library_grid.get_model().get_n_items()):
if self.library_grid.get_model().is_selected(i):
albums.append(self.library_grid.get_model().get_item(i).get_album().get_id())
return albums

9
src/main.py Normal file
View file

@ -0,0 +1,9 @@
import sys
from .application import Application
def main(version):
app = Application()
return app.run(sys.argv)

25
src/mcg.in Executable file
View file

@ -0,0 +1,25 @@
#!@PYTHON@
import os
import sys
import signal
import locale
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
sys.path.insert(1, pkgdatadir)
signal.signal(signal.SIGINT, signal.SIG_DFL)
locale.bindtextdomain('mcg', localedir)
locale.textdomain('mcg')
if __name__ == '__main__':
import gi
from gi.repository import Gio
resource = Gio.Resource.load(os.path.join(pkgdatadir, 'mcg.gresource'))
resource._register()
from mcg import main
sys.exit(main.main(VERSION))

37
src/meson.build Normal file
View file

@ -0,0 +1,37 @@
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
moduledir = join_paths(pkgdatadir, 'mcg')
python = import('python')
conf = configuration_data()
conf.set('PYTHON', python.find_installation('python3').full_path())
conf.set('VERSION', meson.project_version())
conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
conf.set('pkgdatadir', pkgdatadir)
configure_file(
input: 'mcg.in',
output: 'mcg',
configuration: conf,
install: true,
install_dir: get_option('bindir')
)
mcg_sources = [
'__init__.py',
'main.py',
'albumheaderbar.py',
'application.py',
'client.py',
'connectionpanel.py',
'coverpanel.py',
'librarypanel.py',
'playlistpanel.py',
'serverpanel.py',
'shortcutsdialog.py',
'utils.py',
'window.py',
'zeroconf.py',
]
install_data(mcg_sources, install_dir: moduledir)

308
src/playlistpanel.py Normal file
View file

@ -0,0 +1,308 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
import logging
import math
import threading
from gi.repository import Gtk, Gdk, Gio, GObject, GdkPixbuf, Adw
from mcg import client
from mcg.albumheaderbar import AlbumHeaderbar
from mcg.utils import Utils
from mcg.utils import GridItem
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/playlist-panel.ui')
class PlaylistPanel(Adw.Bin):
__gtype_name__ = 'McgPlaylistPanel'
__gsignals__ = {
'open-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()),
'close-standalone': (GObject.SIGNAL_RUN_FIRST, None, ()),
'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()),
'remove-album': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'remove-multiple-albums': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
}
# Widgets
playlist_stack = Gtk.Template.Child()
panel_normal = Gtk.Template.Child()
panel_standalone = Gtk.Template.Child()
actionbar_revealer = Gtk.Template.Child()
# Toolbar
toolbar = Gtk.Template.Child()
playlist_clear_button = Gtk.Template.Child()
select_button = Gtk.Template.Child()
# Playlist Grid
playlist_grid = Gtk.Template.Child()
# Action bar (normal)
actionbar = Gtk.Template.Child()
actionbar_standalone = Gtk.Template.Child()
# Standalone Image
standalone_stack = Gtk.Template.Child()
standalone_spinner = Gtk.Template.Child()
standalone_scroll = Gtk.Template.Child()
standalone_image = Gtk.Template.Child()
def __init__(self, client, **kwargs):
super().__init__(**kwargs)
self._client = client
self._host = None
self._item_size = 150
self._playlist = None
self._playlist_albums = None
self._playlist_lock = threading.Lock()
self._playlist_stop = threading.Event()
self._icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
self._standalone_pixbuf = None
self._selected_albums = []
self._is_selected = False
# Widgets
# Header bar
self._headerbar_standalone = AlbumHeaderbar()
self._headerbar_standalone.connect('close', self.on_headerbar_close_clicked)
# Playlist Grid: Model
self._playlist_grid_model = Gio.ListStore()
self._playlist_grid_selection_multi = Gtk.MultiSelection.new(self._playlist_grid_model)
self._playlist_grid_selection_single = Gtk.SingleSelection.new(self._playlist_grid_model)
# Playlist Grid
self.playlist_grid.set_model(self._playlist_grid_selection_single)
def get_headerbar_standalone(self):
return self._headerbar_standalone
def get_toolbar(self):
return self.toolbar
def set_selected(self, selected):
self._is_selected = selected
@Gtk.Template.Callback()
def on_select_toggled(self, widget):
if self.select_button.get_active():
self.actionbar_revealer.set_reveal_child(True)
self.playlist_grid.set_model(self._playlist_grid_selection_multi)
self.playlist_grid.set_single_click_activate(False)
self.playlist_grid.get_style_context().add_class(Utils.CSS_SELECTION)
else:
self.actionbar_revealer.set_reveal_child(False)
self.playlist_grid.set_model(self._playlist_grid_selection_single)
self.playlist_grid.set_single_click_activate(True)
self.playlist_grid.get_style_context().remove_class(Utils.CSS_SELECTION)
@Gtk.Template.Callback()
def on_clear_clicked(self, widget):
self.emit('clear-playlist')
@Gtk.Template.Callback()
def on_playlist_grid_clicked(self, widget, position):
# Get selected album
item = self._playlist_grid_model.get_item(position)
album = item.get_album()
id = album.get_id()
self._selected_albums = [album]
self.emit('albumart', id)
# Show standalone album
if widget.get_model() == self._playlist_grid_selection_single:
# Set labels
self._headerbar_standalone.set_album(album)
# Show panel
self._open_standalone()
# Set cover loading indicator
self.standalone_stack.set_visible_child(self.standalone_spinner)
self.standalone_spinner.start()
@Gtk.Template.Callback()
def on_selection_cancel_clicked(self, widget):
self.select_button.set_active(False)
@Gtk.Template.Callback()
def on_selection_remove_clicked(self, widget):
self.emit('remove-multiple-albums', self._get_selected_albums())
self.select_button.set_active(False)
def on_headerbar_close_clicked(self, widget):
self._close_standalone()
@Gtk.Template.Callback()
def on_standalone_remove_clicked(self, widget):
self.emit('remove-album', self._get_selected_albums()[0])
self._close_standalone()
@Gtk.Template.Callback()
def on_standalone_play_clicked(self, widget):
self.emit('play', self._get_selected_albums()[0])
self._close_standalone()
def set_size(self, width, height):
self._resize_standalone_image()
def set_item_size(self, item_size):
if self._item_size != item_size:
self._item_size = item_size
self._redraw()
def get_item_size(self):
return self._item_size
def set_playlist(self, host, playlist):
self._host = host
self._playlist_stop.set()
threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
def set_albumart(self, album, data):
if album in self._selected_albums:
if data:
# Load image and draw it
try:
self._standalone_pixbuf = Utils.load_pixbuf(data)
except Exception as e:
self._logger.exception("Failed to set albumart")
self._cover_pixbuf = self._get_default_image()
else:
self._cover_pixbuf = self._get_default_image()
# Show image
GObject.idle_add(self._show_image)
def stop_threads(self):
self._playlist_stop.set()
def _set_playlist(self, host, playlist, size):
self._playlist_lock.acquire()
self._playlist_stop.clear()
self._playlist = playlist
self._playlist_albums = {}
for album in playlist:
self._playlist_albums[album.get_id()] = album
self._playlist_grid_model.remove_all()
cache = client.MCGCache(host, size)
for album in playlist:
pixbuf = None
# Load albumart thumbnail
try:
pixbuf = Utils.load_thumbnail(cache, self._client, album, size)
except client.CommandException:
# Exception is handled by client
pass
except Exception:
self._logger.exception("Failed to load albumart")
if pixbuf is None:
pixbuf = self._icon_theme.lookup_icon(
Utils.STOCK_ICON_DEFAULT,
None,
self._item_size,
self._item_size,
Gtk.TextDirection.LTR,
Gtk.IconLookupFlags.FORCE_SYMBOLIC
)
if pixbuf is not None:
self._playlist_grid_model.append(GridItem(album, pixbuf))
if self._playlist_stop.is_set():
self._playlist_lock.release()
return
self.playlist_grid.set_model(self._playlist_grid_selection_single)
self._playlist_lock.release()
def _show_image(self):
self._resize_standalone_image()
self.standalone_stack.set_visible_child(self.standalone_scroll)
self.standalone_spinner.stop()
def _redraw(self):
if self._playlist is not None:
self.set_playlist(self._host, self._playlist)
def _open_standalone(self):
self.playlist_stack.set_visible_child(self.panel_standalone)
self.emit('open-standalone')
def _close_standalone(self):
self.playlist_stack.set_visible_child(self.panel_normal)
self.emit('close-standalone')
def _resize_standalone_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
# Get size
size_width = self.standalone_stack.get_width()
size_height = self.standalone_stack.get_height()
# Get pixelbuffer
pixbuf = self._standalone_pixbuf
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size_width) / float(pixbuf.get_width())
ratioH = float(size_height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self.standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self.standalone_image.show()
def _get_default_image(self):
return self._icon_theme.lookup_icon(
Utils.STOCK_ICON_DEFAULT,
None,
512,
512,
Gtk.TextDirection.LTR,
Gtk.IconLookupFlags.FORCE_SYMBOLIC
)
def _get_selected_albums(self):
albums = []
for i in range(self.playlist_grid.get_model().get_n_items()):
if self.playlist_grid.get_model().is_selected(i):
albums.append(self.playlist_grid.get_model().get_item(i).get_album())
return albums

118
src/serverpanel.py Normal file
View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, GObject
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/server-panel.ui')
class ServerPanel(Adw.Bin):
__gtype_name__ = 'McgServerPanel'
__gsignals__ = {
'change-output-device': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,bool,)),
}
# Widgets
toolbar = Gtk.Template.Child()
# Status widgets
status_file = Gtk.Template.Child()
status_audio = Gtk.Template.Child()
status_bitrate = Gtk.Template.Child()
status_error = Gtk.Template.Child()
# Stats widgets
stats_artists = Gtk.Template.Child()
stats_albums = Gtk.Template.Child()
stats_songs = Gtk.Template.Child()
stats_dbplaytime = Gtk.Template.Child()
stats_playtime = Gtk.Template.Child()
stats_uptime = Gtk.Template.Child()
# Audio ouptut devices widgets
output_devices = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._none_label = ""
self._output_buttons = {}
self._is_selected = False
# Widgets
self._none_label = self.status_file.get_label()
def set_selected(self, selected):
self._is_selected = selected
def get_toolbar(self):
return self.toolbar
def on_output_device_toggled(self, widget, device):
self.emit('change-output-device', device, widget.get_active())
def set_status(self, file, audio, bitrate, error):
if file:
file = GObject.markup_escape_text(file)
else:
file = self._none_label
self.status_file.set_markup(file)
# Audio information
if audio:
parts = audio.split(":")
if len(parts) == 3:
audio = "{}Hz, {}bit, {}channels".format(parts[0], parts[1], parts[2])
else:
audio = self._none_label
self.status_audio.set_markup(audio)
# Bitrate
if bitrate:
bitrate = bitrate + "kb/s"
else:
bitrate = self._none_label
self.status_bitrate.set_markup(bitrate)
# Error
if error:
error = GObject.markup_escape_text(error)
else:
error = self._none_label
self.status_error.set_markup(error)
def set_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
self.stats_artists.set_text(str(artists))
self.stats_albums.set_text(str(albums))
self.stats_songs.set_text(str(songs))
self.stats_dbplaytime.set_text(str(dbplaytime))
self.stats_playtime.set_text(str(playtime))
self.stats_uptime.set_text(str(uptime))
def set_output_devices(self, devices):
device_ids = []
# Add devices
for device in devices:
device_ids.append(device.get_id())
if device.get_id() in self._output_buttons.keys():
self._output_buttons[device.get_id()].freeze_notify()
self._output_buttons[device.get_id()].set_active(device.is_enabled())
self._output_buttons[device.get_id()].thaw_notify()
else:
button = Gtk.CheckButton.new_with_label(device.get_name())
if device.is_enabled():
button.set_active(True)
handler = button.connect('toggled', self.on_output_device_toggled, device)
self.output_devices.insert(button, -1)
self._output_buttons[device.get_id()] = button
# Remove devices
for id in self._output_buttons.keys():
if id not in device_ids:
self.output_devices.remove(self._output_buttons[id].get_parent())

18
src/shortcutsdialog.py Normal file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/shortcuts-dialog.ui')
class ShortcutsDialog(Gtk.ShortcutsWindow):
__gtype_name__ = 'McgShortcutsDialog'
def __init__(self):
super().__init__()

133
src/utils.py Normal file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
import hashlib
import locale
import os
import urllib
from gi.repository import Gdk, GdkPixbuf, GObject, Gtk
class Utils:
CSS_SELECTION = 'selection'
STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
def load_pixbuf(data):
loader = GdkPixbuf.PixbufLoader()
try:
loader.write(data)
finally:
loader.close()
return loader.get_pixbuf()
def load_thumbnail(cache, client, album, size):
cache_url = cache.create_filename(album)
pixbuf = None
if os.path.isfile(cache_url):
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url)
else:
# Load cover from server
albumart = client.get_albumart_now(album.get_id())
if albumart:
pixbuf = Utils.load_pixbuf(albumart)
if pixbuf is not None:
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER)
pixbuf.savev(cache_url, 'jpeg', [], [])
return pixbuf
def create_artists_label(album):
label = ', '.join(album.get_albumartists())
if album.get_artists():
label = locale.gettext("{} feat. {}").format(
label,
", ".join(album.get_artists())
)
return label
def create_length_label(album):
minutes = album.get_length() // 60
seconds = album.get_length() - minutes * 60
return locale.gettext("{}:{} minutes").format(minutes, seconds)
def create_track_title(track):
title = track.get_title()
if track.get_artists():
title = locale.gettext("{} feat. {}").format(
title,
", ".join(track.get_artists())
)
return title
def generate_id(values):
if type(values) is not list:
values = [values]
m = hashlib.md5()
for value in values:
m.update(value.encode('utf-8'))
return m.hexdigest()
class SortOrder:
ARTIST = 0
TITLE = 1
YEAR = 2
MODIFIED = 3
class GridItem(GObject.GObject):
__gtype_name__ = "GridItem"
tooltip = GObject.Property(type=str, default=None)
cover = GObject.Property(type=Gdk.Paintable, default=None)
def __init__(self, album, cover):
super().__init__()
self._album = album
if cover:
self.cover = Gdk.Texture.new_for_pixbuf(cover)
self.tooltip = GObject.markup_escape_text("\n".join([
album.get_title(),
', '.join(album.get_dates()),
Utils.create_artists_label(album),
Utils.create_length_label(album)
]))
def get_album(self):
return self._album
def set_cover(self, cover):
self.cover = Gdk.Texture.new_for_pixbuf(cover)
class SearchFilter(Gtk.Filter):
def __init__(self, search_string):
super().__init__()
self._search_string = search_string
def do_match(self, grid_item):
return grid_item.get_album().filter(self._search_string)

595
src/window.py Normal file
View file

@ -0,0 +1,595 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
try:
import keyring
use_keyring = True
except:
use_keyring = False
use_keyring = False
import locale
import logging
from gi.repository import Gtk, Adw, Gdk, GObject, GLib, Gio
from . import client
from .shortcutsdialog import ShortcutsDialog
from .connectionpanel import ConnectionPanel
from .serverpanel import ServerPanel
from .coverpanel import CoverPanel
from .playlistpanel import PlaylistPanel
from .librarypanel import LibraryPanel
from .zeroconf import ZeroconfProvider
class WindowState(GObject.Object):
WIDTH = 'width'
HEIGHT = 'height'
IS_MAXIMIZED = 'is_maximized'
IS_FULLSCREENED = 'is_fullscreened'
width = GObject.Property(type=int, default=800)
height = GObject.Property(type=int, default=600)
is_maximized = GObject.Property(type=bool, default=False)
is_fullscreened = GObject.Property(type=bool, default=False)
def __init__(self):
super().__init__()
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/window.ui')
class Window(Adw.ApplicationWindow):
__gtype_name__ = 'McgAppWindow'
SETTING_HOST = 'host'
SETTING_PORT = 'port'
SETTING_CONNECTED = 'connected'
SETTING_WINDOW_WIDTH = 'width'
SETTING_WINDOW_HEIGHT = 'height'
SETTING_WINDOW_MAXIMIZED = 'is-maximized'
SETTING_PANEL = 'panel'
SETTING_ITEM_SIZE = 'item-size'
SETTING_SORT_ORDER = 'sort-order'
SETTING_SORT_TYPE = 'sort-type'
# Widgets
toolbar_view = Gtk.Template.Child()
content_stack = Gtk.Template.Child()
panel_stack = Gtk.Template.Child()
toolbar_stack = Gtk.Template.Child()
# Headerbar
headerbar = Gtk.Template.Child()
headerbar_panel_switcher = Gtk.Template.Child()
headerbar_button_connect = Gtk.Template.Child()
headerbar_button_playpause = Gtk.Template.Child()
headerbar_button_volume = Gtk.Template.Child()
# Infobar
info_toast = Gtk.Template.Child()
def __init__(self, app, title, settings, **kwargs):
super().__init__(**kwargs)
self.set_application(app)
self.set_title(title)
self._settings = settings
self._panels = []
self._mcg = client.Client()
self._state = WindowState()
self._setting_volume = False
self._headerbar_connection_button_active = True
self._headerbar_playpause_button_active = True
# Help/Shortcuts dialog
self.set_help_overlay(ShortcutsDialog())
# Login screen
self._connection_panel = ConnectionPanel()
# Server panel
self._server_panel = ServerPanel()
self._panels.append(self._server_panel)
# Cover panel
self._cover_panel = CoverPanel()
self._panels.append(self._cover_panel)
# Playlist panel
self._playlist_panel = PlaylistPanel(self._mcg)
self._playlist_panel.connect('open-standalone', self.on_panel_open_standalone)
self._playlist_panel.connect('close-standalone', self.on_panel_close_standalone)
self._panels.append(self._playlist_panel)
# Library panel
self._library_panel = LibraryPanel(self._mcg)
self._library_panel.connect('open-standalone', self.on_panel_open_standalone)
self._library_panel.connect('close-standalone', self.on_panel_close_standalone)
self._panels.append(self._library_panel)
# Stack
self.content_stack.add_child(self._connection_panel)
self.panel_stack.add_titled_with_icon(self._server_panel, 'server-panel', locale.gettext("Server"), "network-wired-symbolic")
self.panel_stack.add_titled_with_icon(self._cover_panel, 'cover-panel', locale.gettext("Cover"), "image-x-generic-symbolic")
self.panel_stack.add_titled_with_icon(self._playlist_panel, 'playlist-panel', locale.gettext("Playlist"), "view-list-symbolic")
self.panel_stack.add_titled_with_icon(self._library_panel, 'library-panel', locale.gettext("Library"), "emblem-music-symbolic")
# Toolbar stack
self.toolbar_stack.add_child(self._server_panel.get_toolbar())
self.toolbar_stack.add_child(self._cover_panel.get_toolbar())
self.toolbar_stack.add_child(self._playlist_panel.get_toolbar())
self.toolbar_stack.add_child(self._library_panel.get_toolbar())
# Properties
self._set_headerbar_sensitive(False, False)
self._connection_panel.set_host(self._settings.get_string(Window.SETTING_HOST))
self._connection_panel.set_port(self._settings.get_int(Window.SETTING_PORT))
if use_keyring:
self._connection_panel.set_password(keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME))
self._playlist_panel.set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
self._library_panel.set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
self._library_panel.set_sort_order(self._settings.get_enum(Window.SETTING_SORT_ORDER))
self._library_panel.set_sort_type(self._settings.get_boolean(Window.SETTING_SORT_TYPE))
# Signals
self.connect("notify::default-width", self.on_resize)
self.connect("notify::default-height", self.on_resize)
self.connect("notify::maximized", self.on_maximized)
self.connect("notify::fullscreened", self.on_fullscreened)
self._connection_panel.connect('connection-changed', self.on_connection_panel_connection_changed)
self.panel_stack.connect('notify::visible-child', self.on_stack_switched)
self._server_panel.connect('change-output-device', self.on_server_panel_output_device_changed)
self._cover_panel.connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen)
self._cover_panel.connect('set-song', self.on_cover_panel_set_song)
self._cover_panel.connect('albumart', self.on_cover_panel_albumart)
self._playlist_panel.connect('clear-playlist', self.on_playlist_panel_clear_playlist)
self._playlist_panel.connect('remove-album', self.on_playlist_panel_remove)
self._playlist_panel.connect('remove-multiple-albums', self.on_playlist_panel_remove_multiple)
self._playlist_panel.connect('play', self.on_playlist_panel_play)
self._playlist_panel.connect('albumart', self.on_playlist_panel_albumart)
self._library_panel.connect('update', self.on_library_panel_update)
self._library_panel.connect('play', self.on_library_panel_play)
self._library_panel.connect('queue', self.on_library_panel_queue)
self._library_panel.connect('queue-multiple', self.on_library_panel_queue_multiple)
self._library_panel.connect('item-size-changed', self.on_library_panel_item_size_changed)
self._library_panel.connect('sort-order-changed', self.on_library_panel_sort_order_changed)
self._library_panel.connect('sort-type-changed', self.on_library_panel_sort_type_changed)
self._library_panel.connect('albumart', self.on_library_panel_albumart)
self._mcg.connect_signal(client.Client.SIGNAL_CONNECTION, self.on_mcg_connect)
self._mcg.connect_signal(client.Client.SIGNAL_STATUS, self.on_mcg_status)
self._mcg.connect_signal(client.Client.SIGNAL_STATS, self.on_mcg_stats)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_OUTPUT_DEVICES, self.on_mcg_load_output_devices)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
self._mcg.connect_signal(client.Client.SIGNAL_PULSE_ALBUMS, self.on_mcg_pulse_albums)
self._mcg.connect_signal(client.Client.SIGNAL_INIT_ALBUMS, self.on_mcg_init_albums)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMART, self.on_mcg_load_albumart)
self._mcg.connect_signal(client.Client.SIGNAL_ERROR, self.on_mcg_error)
self._settings.connect('changed::'+Window.SETTING_PANEL, self.on_settings_panel_changed)
self._settings.connect('changed::'+Window.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
self._settings.connect('changed::'+Window.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
self._settings.connect('changed::'+Window.SETTING_SORT_TYPE, self.on_settings_sort_type_changed)
self._settings.bind(Window.SETTING_WINDOW_WIDTH, self._state, WindowState.WIDTH, Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_HEIGHT, self._state, WindowState.HEIGHT, Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_MAXIMIZED, self._state, WindowState.IS_MAXIMIZED, Gio.SettingsBindFlags.DEFAULT)
# Actions
self.set_default_size(self._state.width, self._state.height)
if self._state.get_property(WindowState.IS_MAXIMIZED):
self.maximize()
self.content_stack.set_visible_child(self._connection_panel)
if self._settings.get_boolean(Window.SETTING_CONNECTED):
self._connect()
# Menu actions
self._connect_action = Gio.SimpleAction.new_stateful("connect", None, GLib.Variant.new_boolean(False))
self._connect_action.connect('change-state', self.on_menu_connect)
self.add_action(self._connect_action)
self._play_action = Gio.SimpleAction.new_stateful("play", None, GLib.Variant.new_boolean(False))
self._play_action.set_enabled(False)
self._play_action.connect('change-state', self.on_menu_play)
self.add_action(self._play_action)
self._clear_playlist_action = Gio.SimpleAction.new("clear-playlist", None)
self._clear_playlist_action.set_enabled(False)
self._clear_playlist_action.connect('activate', self.on_menu_clear_playlist)
self.add_action(self._clear_playlist_action)
panel_variant = GLib.Variant.new_string("0")
self._panel_action = Gio.SimpleAction.new_stateful("panel", panel_variant.get_type(), panel_variant)
self._panel_action.set_enabled(False)
self._panel_action.connect('change-state', self.on_menu_panel)
self.add_action(self._panel_action)
self._toggle_fullscreen_action = Gio.SimpleAction.new("toggle-fullscreen", None)
self._toggle_fullscreen_action.set_enabled(True)
self._toggle_fullscreen_action.connect('activate', self.on_menu_toggle_fullscreen)
self.add_action(self._toggle_fullscreen_action)
self._search_library_action = Gio.SimpleAction.new("search-library", None)
self._search_library_action.set_enabled(True)
self._search_library_action.connect('activate', self.on_menu_search_library)
self.add_action(self._search_library_action)
# Menu callbacks
def on_menu_connect(self, action, value):
self._connect()
def on_menu_play(self, action, value):
self._mcg.playpause()
def on_menu_clear_playlist(self, action, value):
self._mcg.clear_playlist()
def on_menu_panel(self, action, value):
action.set_state(value)
self.panel_stack.set_visible_child(self._panels[int(value.get_string())])
def on_menu_toggle_fullscreen(self, action, value):
self.panel_stack.set_visible_child(self._cover_panel)
if not self._state.get_property(WindowState.IS_FULLSCREENED):
self.fullscreen()
else:
self.unfullscreen()
def on_menu_search_library(self, action, value):
self.panel_stack.set_visible_child(self.library_panel_page)
self._library_panel.show_search()
# Window callbacks
def on_resize(self, widget, event):
width = self.get_size(Gtk.Orientation.HORIZONTAL)
height = self.get_size(Gtk.Orientation.VERTICAL)
if width > 0:
self._cover_panel.set_width(width)
if not self._state.get_property(WindowState.IS_MAXIMIZED):
self._state.set_property(WindowState.WIDTH, width)
self._state.set_property(WindowState.HEIGHT, height)
GObject.idle_add(self._playlist_panel.set_size, width, height)
GObject.idle_add(self._library_panel.set_size, width, height)
def on_maximized(self, widget, maximized):
self._state.set_property(WindowState.IS_MAXIMIZED, maximized is True)
def on_fullscreened(self, widget, fullscreened):
self._fullscreen(self.is_fullscreen())
# HeaderBar callbacks
@Gtk.Template.Callback()
def on_headerbar_connection_state_set(self, widget, state):
if self._headerbar_connection_button_active:
self._connect()
@Gtk.Template.Callback()
def on_headerbar_volume_changed(self, widget, value):
if not self._setting_volume:
self._mcg.set_volume(int(value*100))
@Gtk.Template.Callback()
def on_headerbar_playpause_toggled(self, widget):
if self._headerbar_playpause_button_active:
self._mcg.playpause()
self._mcg.get_status()
# Panel callbacks
def on_stack_switched(self, widget, prop):
self._set_visible_toolbar()
self._save_visible_panel()
self._set_menu_visible_panel()
for panel in self._panels:
panel.set_selected(panel == self.panel_stack.get_visible_child())
def on_panel_open_standalone(self, panel):
self.toolbar_view.add_top_bar(panel.get_headerbar_standalone())
self.toolbar_view.remove(self.headerbar)
def on_panel_close_standalone(self, panel):
self.toolbar_view.add_top_bar(self.headerbar)
self.toolbar_view.remove(panel.get_headerbar_standalone())
def on_connection_panel_connection_changed(self, widget, host, port, password):
self._settings.set_string(Window.SETTING_HOST, host)
self._settings.set_int(Window.SETTING_PORT, port)
if use_keyring:
if password:
keyring.set_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME, password)
else:
if keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME):
keyring.delete_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME)
def on_playlist_panel_clear_playlist(self, widget):
self._mcg.clear_playlist()
def on_playlist_panel_remove(self, widget, album):
self._mcg.remove_album_from_playlist(album)
def on_playlist_panel_remove_multiple(self, widget, albums):
self._mcg.remove_albums_from_playlist(albums)
def on_playlist_panel_play(self, widget, album):
self._mcg.play_album_from_playlist(album)
def on_playlist_panel_albumart(self, widget, album):
self._mcg.get_albumart(album)
def on_server_panel_output_device_changed(self, widget, device, enabled):
self._mcg.enable_output_device(device, enabled)
def on_cover_panel_toggle_fullscreen(self, widget):
if not self._state.get_property(WindowState.IS_FULLSCREENED):
self.fullscreen()
else:
self.unfullscreen()
def on_cover_panel_set_song(self, widget, pos, time):
self._mcg.seek(pos, time)
def on_cover_panel_albumart(self, widget, album):
self._mcg.get_albumart(album)
def on_library_panel_update(self, widget):
self._mcg.update()
def on_library_panel_play(self, widget, album):
self._mcg.play_album(album)
def on_library_panel_queue(self, widget, album):
self._mcg.queue_album(album)
def on_library_panel_queue_multiple(self, widget, albums):
self._mcg.queue_albums(albums)
def on_library_panel_item_size_changed(self, widget, size):
self._playlist_panel.set_item_size(size)
self._settings.set_int(Window.SETTING_ITEM_SIZE, self._library_panel.get_item_size())
def on_library_panel_sort_order_changed(self, widget, sort_order):
self._settings.set_enum(Window.SETTING_SORT_ORDER, sort_order)
def on_library_panel_sort_type_changed(self, widget, sort_type):
self._settings.set_boolean(Window.SETTING_SORT_TYPE, sort_type)
def on_library_panel_albumart(self, widget, album):
self._mcg.get_albumart(album)
# MCG callbacks
def on_mcg_connect(self, connected):
if connected:
GObject.idle_add(self._connect_connected)
self._mcg.load_playlist()
self._mcg.load_albums()
self._mcg.get_status()
self._mcg.get_stats()
self._mcg.get_output_devices()
self._connect_action.set_state(GLib.Variant.new_boolean(True))
self._play_action.set_enabled(True)
self._clear_playlist_action.set_enabled(True)
self._panel_action.set_enabled(True)
else:
GObject.idle_add(self._connect_disconnected)
self._connect_action.set_state(GLib.Variant.new_boolean(False))
self._play_action.set_enabled(False)
self._clear_playlist_action.set_enabled(False)
self._panel_action.set_enabled(False)
def on_mcg_status(self, state, album, pos, time, volume, file, audio, bitrate, error):
# Album
GObject.idle_add(self._cover_panel.set_album, album)
if not album and self._state.get_property(WindowState.IS_FULLSCREENED):
self._fullscreen(False)
# State
if state == 'play':
GObject.idle_add(self._set_play)
GObject.idle_add(self._cover_panel.set_play, pos, time)
self._play_action.set_state(GLib.Variant.new_boolean(True))
elif state == 'pause' or state == 'stop':
GObject.idle_add(self._set_pause)
GObject.idle_add(self._cover_panel.set_pause)
self._play_action.set_state(GLib.Variant.new_boolean(False))
# Volume
GObject.idle_add(self._set_volume, volume)
# Status
self._server_panel.set_status(file, audio, bitrate, error)
# Error
if error:
self._show_error(error)
def on_mcg_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
self._server_panel.set_stats(artists, albums, songs, dbplaytime, playtime, uptime)
def on_mcg_load_output_devices(self, devices):
self._server_panel.set_output_devices(devices)
def on_mcg_load_playlist(self, playlist):
self._playlist_panel.set_playlist(self._connection_panel.get_host(), playlist)
def on_mcg_init_albums(self):
GObject.idle_add(self._library_panel.init_albums)
def on_mcg_pulse_albums(self):
GObject.idle_add(self._library_panel.load_albums)
def on_mcg_load_albums(self, albums):
self._library_panel.set_albums(self._connection_panel.get_host(), albums)
def on_mcg_load_albumart(self, album, data):
self._cover_panel.set_albumart(album, data)
self._playlist_panel.set_albumart(album, data)
self._library_panel.set_albumart(album, data)
def on_mcg_error(self, error):
GObject.idle_add(self._show_error, str(error))
# Settings callbacks
def on_settings_panel_changed(self, settings, key):
panel_index = settings.get_int(key)
self.panel_stack.set_visible_child(self._panels[panel_index])
def on_settings_item_size_changed(self, settings, key):
size = settings.get_int(key)
self._playlist_panel.set_item_size(size)
self._library_panel.set_item_size(size)
def on_settings_sort_order_changed(self, settings, key):
sort_order = settings.get_enum(key)
self._library_panel.set_sort_order(sort_order)
def on_settings_sort_type_changed(self, settings, key):
sort_type = settings.get_boolean(key)
self._library_panel.set_sort_type(sort_type)
# Private methods
def _connect(self):
self._connection_panel.set_sensitive(False)
self._set_headerbar_sensitive(False, True)
if self._mcg.is_connected():
self._mcg.disconnect()
self._settings.set_boolean(Window.SETTING_CONNECTED, False)
else:
host = self._connection_panel.get_host()
port = self._connection_panel.get_port()
password = self._connection_panel.get_password()
self._mcg.connect(host, port, password)
self._settings.set_boolean(Window.SETTING_CONNECTED, True)
def _connect_connected(self):
self._headerbar_connected()
self._set_headerbar_sensitive(True, False)
self.content_stack.set_visible_child(self.panel_stack)
self.panel_stack.set_visible_child(self._panels[self._settings.get_int(Window.SETTING_PANEL)])
def _connect_disconnected(self):
self._playlist_panel.stop_threads();
self._library_panel.stop_threads();
self._headerbar_disconnected()
self._set_headerbar_sensitive(False, False)
self._save_visible_panel()
self.content_stack.set_visible_child(self._connection_panel)
self._connection_panel.set_sensitive(True)
def _fullscreen(self, fullscreened_new):
if fullscreened_new != self._state.get_property(WindowState.IS_FULLSCREENED):
self._state.set_property(WindowState.IS_FULLSCREENED, fullscreened_new)
if self._state.get_property(WindowState.IS_FULLSCREENED):
self.headerbar.hide()
self._cover_panel.set_fullscreen(True)
self.set_cursor(Gdk.Cursor.new_from_name("none", None))
else:
self.headerbar.show()
self._cover_panel.set_fullscreen(False)
self.set_cursor(Gdk.Cursor.new_from_name("default", None))
def _save_visible_panel(self):
panel_index_selected = self._panels.index(self.panel_stack.get_visible_child())
self._settings.set_int(Window.SETTING_PANEL, panel_index_selected)
def _set_menu_visible_panel(self):
panel_index_selected = self._panels.index(self.panel_stack.get_visible_child())
self._panel_action.set_state(GLib.Variant.new_string(str(panel_index_selected)))
def _set_visible_toolbar(self):
panel_index_selected = self._panels.index(self.panel_stack.get_visible_child())
toolbar = self._panels[panel_index_selected].get_toolbar()
self.toolbar_stack.set_visible_child(toolbar)
def _set_play(self):
self._headerbar_playpause_button_active = False
self.headerbar_button_playpause.set_active(True)
self._headerbar_playpause_button_active = True
def _set_pause(self):
self._headerbar_playpause_button_active = False
self.headerbar_button_playpause.set_active(False)
self._headerbar_playpause_button_active = True
def _set_volume(self, volume):
if volume >= 0:
self.headerbar_button_volume.set_visible(True)
self._setting_volume = True
self.headerbar_button_volume.set_value(volume / 100)
self._setting_volume = False
else:
self.headerbar_button_volume.set_visible(False)
def _headerbar_connected(self):
self._headerbar_connection_button_active = False
self.headerbar_button_connect.set_active(True)
self.headerbar_button_connect.set_state(True)
self._headerbar_connection_button_active = True
def _headerbar_disconnected(self):
self._headerbar_connection_button_active = False
self.headerbar_button_connect.set_active(False)
self.headerbar_button_connect.set_state(False)
self._headerbar_connection_button_active = True
def _set_headerbar_sensitive(self, sensitive, connecting):
self.headerbar_button_playpause.set_sensitive(sensitive)
self.headerbar_button_volume.set_sensitive(sensitive)
self.headerbar_panel_switcher.set_sensitive(sensitive)
self.headerbar_button_connect.set_sensitive(not connecting)
def _show_error(self, message):
self.info_toast.add_toast(Adw.Toast.new(message))