Compare commits

...

59 commits

Author SHA1 Message Date
345e7697ff Bump version to 4.0.2 2026-01-10 16:35:46 +01:00
7d474598e3 Center non-square album covers on grid views (close #112) 2026-01-10 16:34:13 +01:00
9b29f7b274 Preserve aspect ratio of album covers in grid views (close #111) 2026-01-10 16:23:30 +01:00
0a109bc886 Fix handling and logging of thumbnail save failures 2026-01-10 16:04:43 +01:00
9311f9974a Do not try to convert default icon to GDK pixbuf (close #110) 2026-01-10 15:42:24 +01:00
08cd9dbe65 Set pixel size for “standalone” images (close #109) 2026-01-10 15:41:12 +01:00
099adbab8c Bump version to 4.0.1 2025-04-06 17:36:02 +02:00
3d91ab1b35 Scroll library to the beginning after loading items (close #108) 2025-04-06 17:33:16 +02:00
dce1c441a0 Fix READMe to call meson’s “setup” command explicitly 2025-04-06 16:55:05 +02:00
cd4f32e7f2 Fix alignment of tracks on Cover panel (close #106)
GTK 4 centers marks of the Scale widget even when the scale has a vertical
orientation. Unfortunately, the Scale widget does not provide a way to set the
alignment or to access the internal Label widget in any way. To left-align the
labels this commit add a method that traverses the all children of the songs
scale recursively and adjusts the alignment if it is a Label widget.
2025-04-06 16:52:10 +02:00
79b3111fb0 Fix reacting to window resizing (close #107)
Replace the handlers for the “default-width” and “default-height” with an
override to the virtual “size_allocate” method to reliably react to Window
resizing.
2025-04-06 16:03:39 +02:00
f3e3f920a5 Bump version to 4.0 2024-11-24 14:35:40 +01:00
84103229f1 Fix conditions using None (see #103) 2024-11-23 16:35:44 +01:00
ccf68deff7 Remove unused (commented out) code (see #103) 2024-11-23 16:35:19 +01:00
2b1edc715f Mark strings as Regular Expressions (see #103) 2024-11-23 16:29:35 +01:00
4586494f8d Pass version to application (see #103) 2024-11-23 16:26:13 +01:00
188dcb4ef8 fixup! Improve variable names to match Code Style Guide (see #103) 2024-11-03 10:56:25 +01:00
99958eabc9 Fix some comments and remove German ones 2024-11-03 10:55:52 +01:00
bcfd17a8ae Remove unused variables (see #103) 2024-11-03 10:54:41 +01:00
420946d7d4 Declare static methods with annotation (see #103) 2024-11-03 10:42:29 +01:00
54fe825acf Improve variable names to match Code Style Guide (see #103) 2024-05-28 12:03:07 +02:00
4ebfc12f0e Fix import statements and order (see #103) 2024-05-28 11:47:30 +02:00
a1f8b73590 Adjust line length to match Code Style Guide (see #103) 2024-05-28 11:47:30 +02:00
d2e1f6f5d8 Adjust blank lines to match Code Style Guide (see #103) 2024-05-28 11:47:30 +02:00
75b99e5820 Port UI to GTK 4 (close #85) 2024-05-23 13:02:51 +02:00
6ba8bc550f Bump version to 3.2.1 2023-02-28 13:18:07 +01:00
21bd0f5832 Fix setting sort type on Library panel at startup (close #95) 2023-02-28 12:55:26 +01:00
bfb8eac62d Bump version to 3.2 2023-01-21 17:18:41 +01:00
44fb332c62 Read cover with1 “readpicture” command if “albumart” is not found (close #90) 2023-01-21 15:37:29 +01:00
e976e05efe Fix setting sort order on Library panel (close #91) 2023-01-18 17:27:27 +01:00
a1e87e8994 Add new dependency “python-dateutil” to README 2023-01-18 17:27:27 +01:00
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
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
48b1d90fc8 Bump version to 3.1 2022-09-11 18:13:52 +02:00
37b8701361 Use Markdown for README file (close #86) 2022-09-11 12:48:55 +02:00
fac7a85566 Use the build system “meson” (close #32)
Replace the build system “setuptools” with “meson”.
2022-09-11 12:31:13 +02:00
ff0eee8380 Bump version to 3.0.2 2022-09-11 12:28:24 +02:00
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
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
ac14c8c7c7 Bump version to 3.0.1 2021-04-18 18:04:34 +02:00
16030a2053 Fix icon path in setup.py 2021-04-18 18:03:55 +02:00
b90ce3299f Bump version to 3.0 2021-04-18 17:14:04 +02:00
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
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
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
0a631877df Fix setting albumart on UI widgets 2020-10-24 14:58:28 +02:00
8714d7a309 Update translation catalogues 2020-10-24 14:43:00 +02:00
83082c3265 Add back shortcut to search the library 2020-10-24 14:37:38 +02:00
32d02f2d9b Update GTK resources and schema to new domain (close #66) 2020-08-09 11:09:53 +02:00
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
f4b545369c Merge release v2.1.2 2020-08-03 16:29:17 +02:00
17fe4ee8ca Bump version to 2.1.2 2020-08-03 16:27:31 +02:00
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
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
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
83990c8796 Bump version to 2.1.1 2020-07-26 09:26:57 +02:00
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
56 changed files with 4973 additions and 5801 deletions

6
.gitignore vendored
View file

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

64
README.md Normal file
View file

@ -0,0 +1,64 @@
# 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) 4 (>= 4.18) ([python-gobject](https://live.gnome.org/PyGObject))
* [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/) (>= 1.2)
* [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 setup --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://suruatoel.xyz/images/mcg-cover-s.png(CoverGrid\u2019s cover panel with album details and track list.)!
!https://suruatoel.xyz/images/mcg-playlist.png(CoverGrid\u2019s playlist panel with queued albums.)!
!https://suruatoel.xyz/images/mcg-library-m.png(CoverGrid\u2019s library panel showing the albums middle-sized.)!
!https://suruatoel.xyz/images/mcg-library-s.png(CoverGrid\u2019s library panel showing the albums 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);
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 {
background:none;
}
@ -18,13 +30,17 @@
font-weight:bold;
}
revealer.sidebar > * {
background-color:alpha(@theme_bg_color, 0.8);
box-shadow:0 0 10px @theme_bg_color;
margin-left:20px
window.fullscreen #cover_box {
background: black;
}
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;
}
@ -32,24 +48,13 @@ actionbar {
background-color:@theme_unfocused_bg_color;
}
/* Icon View in regular mode */
iconview.view:selected,
iconview.view:selected:focus {
background-color:@theme_selected_bg_color;
gridview child {
padding: 1px;
}
iconview.view:hover {
-gtk-icon-effect:highlight;
gridview.selection child {
opacity: 0.5;
}
/* Icon View in selection mode */
iconview.view.selection {
-gtk-icon-effect:dim;
}
iconview.view.selection:selected,
iconview.view.selection:selected:focus {
background-color:@theme_selected_bg_color;
-gtk-icon-effect:highlight;
}
iconview.view.selection:hover {
-gtk-icon-effect:none;
gridview.selection child:hover,
gridview.selection child:selected {
opacity: 1;
}

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"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
@ -9,81 +7,82 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93334"
version="1.1"
sodipodi:docname="mcg.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg8"
inkscape:version="0.92.1 r"
sodipodi:docname="mcg.svg">
version="1.1"
viewBox="0 0 67.733333 67.733335"
height="256"
width="256">
<title
id="title4619">CoverGrid (mcg)</title>
<defs
id="defs2">
<filter
style="color-interpolation-filters:sRGB;"
id="filter4516"
inkscape:label="Drop Shadow"
id="filter4516">
style="color-interpolation-filters:sRGB">
<feFlood
flood-opacity="0.498039"
flood-color="rgb(255,255,255)"
id="feFlood4506"
result="flood"
id="feFlood4506" />
flood-color="rgb(255,255,255)"
flood-opacity="0.498039" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
id="feComposite4508"
result="composite1"
id="feComposite4508" />
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
in="composite1"
stdDeviation="7"
id="feGaussianBlur4510"
result="blur"
id="feGaussianBlur4510" />
stdDeviation="7"
in="composite1" />
<feOffset
dx="6"
dy="6"
id="feOffset4512"
result="offset"
id="feOffset4512" />
dy="6"
dx="6" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
id="feComposite4514"
result="composite2"
id="feComposite4514" />
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
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:document-rotation="0"
inkscape:snap-to-guides="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
position="319.76785,-21.166667"
orientation="0,1"
inkscape:locked="false"
id="guide4676"
inkscape:locked="false" />
orientation="0,1"
position="319.76785,-21.166667" />
</sodipodi:namedview>
<metadata
id="metadata5">
@ -120,94 +119,95 @@
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
transform="translate(0,-26.06665)"
id="layer1"
transform="translate(0,-26.06665)">
inkscape:groupmode="layer"
inkscape:label="Ebene 1">
<g
id="g4504"
transform="matrix(0.8231966,0,0,0.8231966,23.950969,28.559636)">
transform="matrix(0.253073,0,0,0.25346533,-0.4687123,18.990219)"
id="g4504">
<rect
y="117.61248"
x="91.54583"
height="87.841667"
width="87.841667"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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>
<path
sodipodi:type="star"
style="opacity:0.57999998;fill:#000000;fill-opacity:1;stroke-width:0.98095167;filter:url(#filter4516)"
id="path4661"
sodipodi:sides="3"
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"
transform="matrix(0.30742717,0,0,0.30790377,0,18.04063)"
inkscape:transform-center-y="-0.54802213"
inkscape:transform-center-x="5.196062"
d="M 204.82033,136.72273 50.691586,224.82232 51.459481,47.293117 Z"
inkscape:randomized="0"
d="M 230.29592,162.19832 76.16718,250.2979 76.935074,72.768703 Z"
inkscape:transform-center-x="16.901764"
inkscape:transform-center-y="-1.779849" />
inkscape:rounded="0"
inkscape:flatsided="true"
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>
</svg>

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Before After
Before After

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
Keywords=mpd;
Type=Application
Icon=mcg.svg
Icon=xyz.suruatoel.mcg
Exec=mcg
Categories=AudioVideo;
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="halign">start</property>
<property name="valign">fill</property>
<property name="vexpand">true</property>
<property name="restrict-to-fill-level">False</property>
<property name="digits">0</property>
<property name="draw-value">False</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

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

@ -0,0 +1,344 @@
<?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>
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkPicture">
<property name="content-fit">contain</property>
<property name="can-shrink">false</property>
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<binding name="tooltip-markup">
<lookup name="tooltip" type="GridItem">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
<binding name="paintable">
<lookup name="cover" type="GridItem">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</child>
</object>
</property>
</template>
</interface>
]]>
</property>
</object>
</property>
</object>
</child>
<style>
<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"?>
<interface>
<requires lib="gtk+" version="3.18"/>
<object class="GtkShortcutsWindow" id="shortcuts-dialog">
<property name="modal">True</property>
<requires lib="gtk+" version="4.8"/>
<requires lib="adw" version="1.2" />
<template class="McgShortcutsDialog" parent="GtkShortcutsWindow">
<property name="modal">1</property>
<child>
<object class="GtkShortcutsSection">
<property name="visible">1</property>
@ -10,53 +11,45 @@
<property name="max-height">10</property>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_1</property>
<property name="title" translatable="yes">Switch to the Connection panel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_2</property>
<property name="title" translatable="yes">Switch to the Cover panel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_3</property>
<property name="title" translatable="yes">Switch to the Playlist panel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;KP_4</property>
<property name="title" translatable="yes">Switch to the Library panel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;k</property>
<property name="title" translatable="yes">Show the keyboard shortcuts</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;i</property>
<property name="title" translatable="yes">Open the info dialog</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;q</property>
<property name="title" translatable="yes">Quit the application</property>
</object>
@ -65,25 +58,21 @@
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">Player</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;c</property>
<property name="title" translatable="yes">Connect or disconnect</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;p</property>
<property name="title" translatable="yes">Switch between play and pause</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;r</property>
<property name="title" translatable="yes">Clear the playlist</property>
</object>
@ -92,12 +81,10 @@
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">Cover Panel</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;alt&gt;Return F11</property>
<property name="accelerator">F11</property>
<property name="title" translatable="yes">Show the cover in fullscreen mode</property>
</object>
</child>
@ -105,11 +92,9 @@
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<property name="title" translatable="yes">Library Panel</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;f</property>
<property name="title" translatable="yes">Search the library</property>
</object>
@ -118,5 +103,5 @@
</child>
</object>
</child>
</object>
</template>
</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"?>
<schemalist>
<enum id="de.coderkun.mcg.SortOrder">
<enum id="xyz.suruatoel.mcg.SortOrder">
<value nick="artist" value="0" />
<value nick="title" value="1" />
<value nick="year" value="2" />
<value nick="modified" value="3" />
</enum>
<schema path="/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">
<default>'localhost'</default>
<summary>MPD host</summary>
@ -16,11 +17,6 @@
<summary>MPD port</summary>
<description>MPD port to connect to</description>
</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">
<default>false</default>
<summary>Connection state</summary>
@ -53,7 +49,7 @@
<summary>Size of library items</summary>
<description>The size of items displayed in the library.</description>
</key>
<key enum="de.coderkun.mcg.SortOrder" name="sort-order">
<key enum="xyz.suruatoel.mcg.SortOrder" name="sort-order">
<default>'year'</default>
<summary>Sort criterium for library items</summary>
<description>The sort criterium of items displayed in the library.</description>

Binary file not shown.

View file

@ -1,322 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"POT-Creation-Date: 2020-03-22 11:18+0100\n"
"PO-Revision-Date: 2020-03-22 11:18+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.3\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:293 data/gtk.shortcuts.ui:74
msgid "Connect or disconnect"
msgstr "Die Verbindung herstellen oder trennen"
#: data/gtk.glade:315 data/gtk.shortcuts.ui:81
msgid "Switch between play and pause"
msgstr "Zwischen Abspielen und Pause wechseln"
#: data/gtk.glade:335
msgid "Adjust the volume"
msgstr "Die Lautstärke anpassen"
#: data/gtk.glade:379
msgid "Connect to MPD"
msgstr "Zu MPD verbinden"
#: data/gtk.glade:421 data/gtk.shortcuts.ui:101
msgid "Show the cover in fullscreen mode"
msgstr "Das Cover im Vollbildmodus anzeigen"
#: data/gtk.glade:470 data/gtk.glade:549
msgid "Select multiple albums"
msgstr "Mehrere Alben auswählen"
#: data/gtk.glade:492 data/gtk.shortcuts.ui:88
msgid "Clear the playlist"
msgstr "Die Wiedergabeliste leeren"
#: data/gtk.glade:526 data/gtk.shortcuts.ui:114
msgid "Search the library"
msgstr "Die Bibliothek durchsuchen"
#: data/gtk.glade:571
msgid "Settings and actions"
msgstr "Einstellungen und Aktionen"
#: data/gtk.glade:676
msgid "Enter hostname or IP address"
msgstr "Hostnamen oder IP-Adresse eingeben"
#: data/gtk.glade:688
msgid "Enter URL or local path"
msgstr "URL oder lokalen Pfad eingeben"
#: data/gtk.glade:701
msgid "Enter password or leave blank"
msgstr "Passwort eingeben oder leer lassen"
#: data/gtk.glade:729
msgid "Host:"
msgstr "Host:"
#: data/gtk.glade:741
msgid "Port:"
msgstr "Port:"
#: data/gtk.glade:753
msgid "Password:"
msgstr "Passwort:"
#: data/gtk.glade:765
msgid "Image Directory:"
msgstr "Bildordner:"
#: data/gtk.glade:866
msgid "File:"
msgstr "Datei:"
#: data/gtk.glade:879
msgid "Audio:"
msgstr "Audio:"
#: data/gtk.glade:892
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/gtk.glade:905
msgid "Error:"
msgstr "Fehler:"
#: data/gtk.glade:917 data/gtk.glade:933 data/gtk.glade:949 data/gtk.glade:965
msgid "<i>none</i>"
msgstr "<i>nichts</i>"
#: data/gtk.glade:1000
msgid "Status"
msgstr "Status"
#: data/gtk.glade:1085
msgid "Albums"
msgstr "Alben"
#: data/gtk.glade:1097
msgid "Songs"
msgstr "Songs"
#: data/gtk.glade:1109
msgid "Artists"
msgstr "Künstler"
#: data/gtk.glade:1133
msgid "Seconds"
msgstr "Sekunden"
#: data/gtk.glade:1180
msgid "Seconds played"
msgstr "Sekunden gespielt"
#: data/gtk.glade:1191
msgid "Seconds running"
msgstr "Sekunden laufend"
#: data/gtk.glade:1222
msgid "Statistics"
msgstr "Statistiken"
#: data/gtk.glade:1292
msgid "Audio Devices"
msgstr "Audiogeräte"
#: data/gtk.glade:1306
msgid "Server"
msgstr "Server"
#: data/gtk.glade:1487 data/gtk.menu.ui:30
msgid "Cover"
msgstr "Cover"
#: data/gtk.glade:1629 data/gtk.menu.ui:36
msgid "Playlist"
msgstr "Wiedergabeliste"
#: data/gtk.glade:1656
msgid "search library"
msgstr "Bibliothek durchsuchen"
#: data/gtk.glade:1881 data/gtk.menu.ui:42
msgid "Library"
msgstr "Bibliothek"
#: data/gtk.glade:1964
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:79
msgid "{} feat. {}"
msgstr "{} mit {}"
#: mcg/utils.py:73
msgid "{}:{} minutes"
msgstr "{}:{} Minuten"
#: mcg/widgets.py:1279 mcg/widgets.py:1620
msgid "cancel"
msgstr "abbrechen"
#: mcg/widgets.py:1296 mcg/widgets.py:1637
msgid "play"
msgstr "abspielen"
#: mcg/widgets.py:1299
msgid "remove"
msgstr "entfernen"
#: mcg/widgets.py:1623 mcg/widgets.py:1640
msgid "queue"
msgstr "einreihen"
#: mcg/widgets.py:1872
msgid "Loading albums"
msgstr "Alben werden geladen"
#: mcg/widgets.py:1954
msgid "Loading images"
msgstr "Bilder werden geladen"
#~ 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)"

Binary file not shown.

View file

@ -1,321 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"POT-Creation-Date: 2020-03-22 11:18+0100\n"
"PO-Revision-Date: 2020-03-22 11:18+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.3\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:293 data/gtk.shortcuts.ui:74
msgid "Connect or disconnect"
msgstr "Connect or disconnect"
#: data/gtk.glade:315 data/gtk.shortcuts.ui:81
msgid "Switch between play and pause"
msgstr "Switch between play and pause"
#: data/gtk.glade:335
msgid "Adjust the volume"
msgstr "Adjust the volume"
#: data/gtk.glade:379
msgid "Connect to MPD"
msgstr "Connect to MPD"
#: data/gtk.glade:421 data/gtk.shortcuts.ui:101
msgid "Show the cover in fullscreen mode"
msgstr "Show the cover in fullscreen mode"
#: data/gtk.glade:470 data/gtk.glade:549
msgid "Select multiple albums"
msgstr "Select multiple albums"
#: data/gtk.glade:492 data/gtk.shortcuts.ui:88
msgid "Clear the playlist"
msgstr "Clear the playlist"
#: data/gtk.glade:526 data/gtk.shortcuts.ui:114
msgid "Search the library"
msgstr "Search the library"
#: data/gtk.glade:571
msgid "Settings and actions"
msgstr "Settings and actions"
#: data/gtk.glade:676
msgid "Enter hostname or IP address"
msgstr "Enter hostname or IP address"
#: data/gtk.glade:688
msgid "Enter URL or local path"
msgstr "Enter URL or local path"
#: data/gtk.glade:701
msgid "Enter password or leave blank"
msgstr "Enter password or leave blank"
#: data/gtk.glade:729
msgid "Host:"
msgstr "Host:"
#: data/gtk.glade:741
msgid "Port:"
msgstr "Port:"
#: data/gtk.glade:753
msgid "Password:"
msgstr "Password:"
#: data/gtk.glade:765
msgid "Image Directory:"
msgstr "Image Directory:"
#: data/gtk.glade:866
msgid "File:"
msgstr "File:"
#: data/gtk.glade:879
msgid "Audio:"
msgstr "Audio:"
#: data/gtk.glade:892
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/gtk.glade:905
msgid "Error:"
msgstr "Error:"
#: data/gtk.glade:917 data/gtk.glade:933 data/gtk.glade:949 data/gtk.glade:965
msgid "<i>none</i>"
msgstr "<i>none</i>"
#: data/gtk.glade:1000
msgid "Status"
msgstr "Status"
#: data/gtk.glade:1085
msgid "Albums"
msgstr "Albums"
#: data/gtk.glade:1097
msgid "Songs"
msgstr "Songs"
#: data/gtk.glade:1109
msgid "Artists"
msgstr "Artists"
#: data/gtk.glade:1133
msgid "Seconds"
msgstr "Seconds"
#: data/gtk.glade:1180
msgid "Seconds played"
msgstr "Seconds"
#: data/gtk.glade:1191
msgid "Seconds running"
msgstr "Seconds running"
#: data/gtk.glade:1222
msgid "Statistics"
msgstr "Statistics"
#: data/gtk.glade:1292
msgid "Audio Devices"
msgstr "Audio Devices"
#: data/gtk.glade:1306
msgid "Server"
msgstr "Server"
#: data/gtk.glade:1487 data/gtk.menu.ui:30
msgid "Cover"
msgstr "Cover"
#: data/gtk.glade:1629 data/gtk.menu.ui:36
msgid "Playlist"
msgstr "Playlist"
#: data/gtk.glade:1656
msgid "search library"
msgstr "search library"
#: data/gtk.glade:1881 data/gtk.menu.ui:42
msgid "Library"
msgstr "Library"
#: data/gtk.glade:1964
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:79
msgid "{} feat. {}"
msgstr "{} feat. {}"
#: mcg/utils.py:73
msgid "{}:{} minutes"
msgstr "{}:{} minutes"
#: mcg/widgets.py:1279 mcg/widgets.py:1620
msgid "cancel"
msgstr "cancel"
#: mcg/widgets.py:1296 mcg/widgets.py:1637
msgid "play"
msgstr "play"
#: mcg/widgets.py:1299
msgid "remove"
msgstr "remove"
#: mcg/widgets.py:1623 mcg/widgets.py:1640
msgid "queue"
msgstr "queue"
#: mcg/widgets.py:1872
msgid "Loading albums"
msgstr "Loading albums"
#: mcg/widgets.py:1954
msgid "Loading images"
msgstr "Loading images"
#~ 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"

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,100 +0,0 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import hashlib
import gettext
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 = gettext.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 gettext.gettext("{}:{} minutes").format(minutes, seconds)
def create_track_title(track):
title = track.get_title()
if track.get_artists():
title = gettext.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: '4.0.2',
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.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"
]
)

View file

@ -1,12 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
# Set environment
srcdir = os.path.abspath(os.path.dirname(__file__))
datadir = os.path.join(srcdir, 'data')
@ -21,21 +17,3 @@ if os.path.exists(localedirdev):
# Set GSettings schema dir (if not set already)
if not os.environ.get('GSETTINGS_SCHEMA_DIR'):
os.environ['GSETTINGS_SCHEMA_DIR'] = datadirdev
class Environment:
"""Wrapper class to access environment settings."""
def get_srcdir():
return srcdir
def get_data(subdir):
return os.path.join(datadir, subdir)
def get_locale():
return localedir

28
src/albumheaderbar.py Normal file
View file

@ -0,0 +1,28 @@
#!/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()))

108
src/application.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
import logging
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gio, Gtk, Gdk, Adw
from .window import Window
class Application(Gtk.Application):
TITLE = "CoverGrid"
ID = 'xyz.suruatoel.mcg'
DOMAIN = 'mcg'
def __init__(self, version):
super().__init__(application_id=Application.ID,
flags=Gio.ApplicationFlags.FLAGS_NONE)
self._window = None
self._info_dialog = None
self._version = version
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(self._version)
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):
style_provider = Gtk.CssProvider()
style_provider.load_from_resource(self._get_resource_path('gtk.css'))
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), style_provider,
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()

File diff suppressed because it is too large Load diff

89
src/connectionpanel.py Normal file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env python3
import gi
import locale
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, 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(),
)

256
src/coverpanel.py Normal file
View file

@ -0,0 +1,256 @@
#!/usr/bin/env python3
import gi
import math
gi.require_version('Gtk', '4.0')
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
click_controller = Gtk.GestureClick()
click_controller.connect('pressed', self.on_cover_box_pressed)
self.cover_box.add_controller(click_controller)
# Button controller for songs scale
button_controller = Gtk.GestureClick()
button_controller.connect('pressed', self.on_songs_scale_pressed)
button_controller.connect('unpaired-release',
self.on_songs_scale_released)
self.songs_scale.add_controller(button_controller)
def get_toolbar(self):
return self.toolbar
def set_selected(self, selected):
"""The cover panel does not use selections"""
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:
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)))
# Align marks
self._align_songs_scale_marks()
def _align_songs_scale_marks(self):
self._align_songs_scale_mark(self.songs_scale)
def _align_songs_scale_mark(self, widget):
child = widget.get_first_child()
while child:
if type(child) is Gtk.Label:
child.set_halign(Gtk.Align.START)
else:
self._align_songs_scale_mark(child)
child = child.get_next_sibling()
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):
# 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:
self.cover_default.set_pixel_size(min(size_width, size_height)/2)
return
# Skalierungswert für Breite und Höhe ermitteln
ratio_w = float(size_width) / float(pixbuf.get_width())
ratio_h = float(size_height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratio_w, ratio_h)
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.set_pixel_size(min(width, height))
self.cover_image.show()

470
src/librarypanel.py Normal file
View file

@ -0,0 +1,470 @@
#!/usr/bin/env python3
import gi
import locale
import logging
import math
import threading
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
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
button_controller = Gtk.GestureClick()
button_controller.connect('unpaired-release',
self.on_grid_scale_released)
self.grid_scale.add_controller(button_controller)
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())
grid_range = self.grid_scale.get_adjustment()
if size < grid_range.get_lower() or size > grid_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())
grid_range = widget.get_adjustment()
if size < grid_range.get_lower() or size > grid_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()
album_id = album.get_id()
self._selected_albums = [album]
self.emit('albumart', album_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):
if sort_type:
sort_type_gtk = Gtk.SortType.DESCENDING
else:
sort_type_gtk = 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:
self._standalone_pixbuf = None
if data:
# Load image and draw it
try:
self._standalone_pixbuf = Utils.load_pixbuf(data)
except Exception:
self._logger.exception("Failed to set albumart")
# 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]
grid_item = GridItem(album)
pixbuf = None
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:
icon = self._get_default_icon(self._item_size, self._item_size)
grid_item.set_icon(icon)
else:
self._grid_pixbufs[album.get_id()] = pixbuf
grid_item.set_cover(pixbuf)
GObject.idle_add(self._library_grid_model.append, grid_item)
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()
GObject.idle_add(self.library_grid.scroll_to, 0, Gtk.ListScrollFlags.NONE, None)
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())
count_min = max(int(width / upper), 1)
count_max = max(int(width / lower), 1)
for index in range(count_min, count_max):
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):
# 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:
icon = self._get_default_icon(size_width, size_height)
self.standalone_image.set_from_paintable(icon)
self.standalone_image.set_pixel_size(min(size_width, size_height)/2)
return
(width, height) = Utils.calculate_size(pixbuf.get_width(),
pixbuf.get_height(), size_width,
size_height)
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.set_pixel_size(min(width, height))
self.standalone_image.show()
def _get_default_icon(self, width, height):
return self._icon_theme.lookup_icon(Utils.STOCK_ICON_DEFAULT, None,
width, height,
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

8
src/main.py Normal file
View file

@ -0,0 +1,8 @@
import sys
from .application import Application
def main(version):
app = Application(version)
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)

278
src/playlistpanel.py Normal file
View file

@ -0,0 +1,278 @@
#!/usr/bin/env python3
import gi
import math
import threading
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
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()
album_id = album.get_id()
self._selected_albums = [album]
self.emit('albumart', album_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:
self._standalone_pixbuf = None
if data:
# Load image and draw it
try:
self._standalone_pixbuf = Utils.load_pixbuf(data)
except Exception:
self._logger.exception("Failed to set albumart")
# 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:
grid_item = GridItem(album)
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:
icon = self._get_default_icon(self._item_size, self._item_size)
grid_item.set_icon(icon)
else:
grid_item.set_cover(pixbuf)
GObject.idle_add(self._playlist_grid_model.append, grid_item)
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):
# 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:
icon = self._get_default_icon(size_width, size_height)
self.standalone_image.set_from_paintable(icon)
self.standalone_image.set_pixel_size(min(size_width, size_height)/2)
return
(width, height) = Utils.calculate_size(pixbuf.get_width(),
pixbuf.get_height(), size_width,
size_height)
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.set_pixel_size(min(width, height))
self.standalone_image.show()
def _get_default_icon(self, width, height):
return self._icon_theme.lookup_icon(Utils.STOCK_ICON_DEFAULT, None,
width, height,
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

115
src/serverpanel.py Normal file
View file

@ -0,0 +1,115 @@
#!/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 output 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)
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())

15
src/shortcutsdialog.py Normal file
View file

@ -0,0 +1,15 @@
#!/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__()

138
src/utils.py Normal file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env python3
import gi
import hashlib
import math
import locale
import logging
import os
gi.require_version('Gtk', '4.0')
from gi.repository import Gdk, GdkPixbuf, GObject, Gtk
class Utils:
CSS_SELECTION = 'selection'
STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
@staticmethod
def load_pixbuf(data):
loader = GdkPixbuf.PixbufLoader()
try:
loader.write(data)
finally:
loader.close()
return loader.get_pixbuf()
@staticmethod
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:
(width, height) = Utils.calculate_size(pixbuf.get_width(),
pixbuf.get_height(),
size, size)
pixbuf = pixbuf.scale_simple(width, height,
GdkPixbuf.InterpType.HYPER)
try:
pixbuf.savev(cache_url, 'jpeg', [], [])
except Exception as e:
logger = logging.getLogger(__name__)
logger.warning("Failed to save thumbnail for album\"%s\": "
"%s", album.get_title(), e)
return pixbuf
@staticmethod
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
@staticmethod
def create_length_label(album):
minutes = album.get_length() // 60
seconds = album.get_length() - minutes * 60
return locale.gettext("{}:{} minutes").format(minutes, seconds)
@staticmethod
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
@staticmethod
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()
@staticmethod
def calculate_size(src_width, src_height, dest_width, dest_height):
ratio_w = float(dest_width) / float(src_width)
ratio_h = float(dest_height) / float(src_height)
ratio = min(min(ratio_w, ratio_h), 1)
if ratio == 1:
return (src_width, src_height)
width = int(math.floor(src_width * ratio))
height = int(math.floor(src_height * ratio))
return (width, height)
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):
super().__init__()
self._album = album
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)
def set_icon(self, icon):
self.cover = icon
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)

615
src/window.py Normal file
View file

@ -0,0 +1,615 @@
#!/usr/bin/env python3
import gi
try:
import keyring
use_keyring = True
except ImportError:
use_keyring = False
use_keyring = False
import locale
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
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):
PROP_WIDTH = 'width'
PROP_HEIGHT = 'height'
PROP_MAXIMIZED = 'is_maximized'
PROP_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
self._width = 0
self._height = 0
# 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::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.PROP_WIDTH,
Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_HEIGHT, self._state,
WindowState.PROP_HEIGHT,
Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_MAXIMIZED, self._state,
WindowState.PROP_MAXIMIZED,
Gio.SettingsBindFlags.DEFAULT)
# Actions
self.set_default_size(self._state.width, self._state.height)
if self._state.get_property(WindowState.PROP_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)
def do_size_allocate(self, width, height, baseline):
Gtk.ApplicationWindow().do_size_allocate(self, width, height, baseline)
if self._width == width and self._height == height:
return
self._width = width
self._height = height
if width > 0:
self._cover_panel.set_width(width)
if not self._state.get_property(WindowState.PROP_MAXIMIZED):
self._state.set_property(WindowState.PROP_WIDTH, width)
self._state.set_property(WindowState.PROP_HEIGHT, height)
GObject.idle_add(self._playlist_panel.set_size, width, height)
GObject.idle_add(self._library_panel.set_size, width, height)
# 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.PROP_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_maximized(self, widget, maximized):
self._state.set_property(WindowState.PROP_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.PROP_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.PROP_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.PROP_FULLSCREENED
):
self._state.set_property(WindowState.PROP_FULLSCREENED,
fullscreened_new)
if self._state.get_property(WindowState.PROP_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))

View file

@ -1,27 +1,23 @@
#!/usr/bin/env python3
import gi
try:
gi.require_version('Avahi', '0.6')
from gi.repository import Avahi
use_avahi = True
except:
except ValueError | ImportError:
use_avahi = False
import logging
from mcg import client
class ZeroconfProvider(client.Base):
KEYRING_SYSTEM = 'mcg'
KEYRING_USERNAME = 'mpd'
SIGNAL_SERVICE_NEW = 'service-new'
TYPE = '_mpd._tcp'
def __init__(self):
client.Base.__init__(self)
self._service_resolvers = []
@ -31,35 +27,45 @@ class ZeroconfProvider(client.Base):
if use_avahi:
self._start_client()
def on_new_service(self, browser, interface, protocol, name, type, domain, flags):
#if not (flags & Avahi.LookupResultFlags.GA_LOOKUP_RESULT_LOCAL):
service_resolver = Avahi.ServiceResolver(interface=interface, protocol=protocol, name=name, type=type, domain=domain, aprotocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC, flags=0,)
def on_new_service(self, browser, interface, protocol, name, type, domain,
flags):
service_resolver = Avahi.ServiceResolver(
interface=interface,
protocol=protocol,
name=name,
type=type,
domain=domain,
aprotocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC,
flags=0,
)
service_resolver.connect('found', self.on_found)
service_resolver.connect('failure', self.on_failure)
service_resolver.attach(self._client)
self._service_resolvers.append(service_resolver)
def on_found(self, resolver, interface, protocol, name, type, domain, host, date, port, *args):
def on_found(self, resolver, interface, protocol, name, type, domain, host,
date, port, *args):
if (host, port) not in self._services.keys():
service = (name,host,port)
self._services[(host,port)] = service
service = (name, host, port)
self._services[(host, port)] = service
self._callback(ZeroconfProvider.SIGNAL_SERVICE_NEW, service)
def on_failure(self, resolver, date):
if resolver in self._service_resolvers:
self._service_resolvers.remove(resolver)
def _start_client(self):
self._logger.info("Starting Avahi client")
self._client = Avahi.Client(flags=0,)
self._client = Avahi.Client(flags=0, )
try:
self._client.start()
# Browser
self._service_browser = Avahi.ServiceBrowser(domain='local', flags=0, interface=-1, protocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC, type=ZeroconfProvider.TYPE)
self._service_browser = Avahi.ServiceBrowser(
domain='local',
flags=0,
interface=-1,
protocol=Avahi.Protocol.GA_PROTOCOL_UNSPEC,
type=ZeroconfProvider.TYPE)
self._service_browser.connect('new_service', self.on_new_service)
self._service_browser.attach(self._client)
except Exception as e: