Compare commits

...

44 commits
v2.0.1 ... main

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

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

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

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

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

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

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

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

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

Before:

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

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

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

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

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

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

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

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

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

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

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

63
README.md Normal file
View file

@ -0,0 +1,63 @@
# CoverGrid
CoverGrid (mcg) is a client for the [Music Player
Daemon](http://www.musicpd.org) (MPD), focusing on albums instead of single
tracks. It is not intended to be a replacement for your favorite MPD client but
an addition to get a better album-experience.
Website: https://www.suruatoel.xyz/codes/mcg
License: [GPL](http://www.gnu.org/licenses/gpl.html) v3
Dependencies:
* [Python](http://www.python.org) 3
* [python-dateutil](https://pypi.org/project/python-dateutil/)
* [GTK](http://www.gtk.org) 3 (>= 3.22) ([python-gobject](https://live.gnome.org/PyGObject))
* [Avahi](http://www.avahi.org) (optional)
* [python-keyring](http://pypi.python.org/pypi/keyring) (optional)
* [meson](https://mesonbuild.com/) and [ninja](https://ninja-build.org/) (building)
Additionally a [MPD](http://www.musicpd.org) server (version >= 0.21.0) is
required at runtime.
## Building
Build the application with _meson_ and _ninja_:
$ meson build
$ ninja -C build
## Running/Testing
For testing the application and running it without (system-wide) installation,
donwload/clone the code, build it with the `--prefix` option and install it
with `ninja`:
$ meson --prefix $(pwd)/install build
$ ninja -C build
$ ninja -C build install
After that you can run it with _mesons_ `devenv` command:
$ meson devenv -C build src/mcg
## Installing
Install the application system-wide with _meson_ (after building):
$ ninja -C build install
Note: _On Linux using the distributions package manager is the preferred way
of installing applications system-wide._
## Screenshots
![CoverGrids cover panel with album details and track list.](https://suruatoel.xyz/images/mcg-cover-s.png "CoverGrids cover panel with album details and track list.")
![CoverGrids playlist panel with queued albums.](https://suruatoel.xyz/images/mcg-playlist.png "CoverGrids playlist panel with queued albums.")
![CoverGrids library panel showing the albums middle-sized.](https://suruatoel.xyz/images/mcg-library-m.png "CoverGrids library panel showing the albums middle-sized.")
![CoverGrids library panel showing the albums small-sized.](https://suruatoel.xyz/images/mcg-library-s.png "CoverGrids library panel showing the albums small-sized.")

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

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,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<template class="McgAlbumHeaderbar" parent="GtkHeaderBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child type="title">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="standalone_title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Title</property>
<property name="selectable">True</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="standalone_artist">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Artist</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_close_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</template>
</interface>

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

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<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>
<template class="McgConnectionPanel" parent="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<!-- n-columns=3 n-rows=6 -->
<object class="GtkGrid">
<property name="width-request">500</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="column-spacing">5</property>
<property name="row-homogeneous">True</property>
<property name="column-homogeneous">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkTreeView" id="zeroconf_list">
<property name="visible">True</property>
<property name="can-focus">True</property>
<signal name="focus-out-event" handler="on_zeroconf_list_outfocused" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<signal name="changed" handler="on_service_selected" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
<property name="height">6</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="host_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="text">localhost</property>
<property name="placeholder-text" translatable="yes">Enter hostname or IP address</property>
<signal name="focus-out-event" handler="on_host_entry_outfocused" swapped="no"/>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="visibility">False</property>
<property name="placeholder-text" translatable="yes">Enter password or leave blank</property>
<property name="input-purpose">password</property>
<signal name="focus-out-event" handler="on_password_entry_outfocused" swapped="no"/>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">5</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="port_spinner">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="text">6600</property>
<property name="input-purpose">number</property>
<property name="adjustment">server-port-adjustment</property>
<property name="value">6600</property>
<signal name="value-changed" handler="on_port_spinner_value_changed" swapped="no"/>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Host:</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Port:</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Password:</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</template>
</interface>

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

@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<template class="McgCoverPanel" parent="GtkOverlay">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkStack" id="cover_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner" id="cover_spinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="name">cover-spinner</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="cover_scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="kinetic-scrolling">False</property>
<property name="overlay-scrolling">False</property>
<signal name="size-allocate" handler="on_cover_size_allocate" swapped="no"/>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkEventBox" id="cover_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<signal name="button-press-event" handler="on_cover_box_pressed" swapped="no"/>
<child>
<object class="GtkImage" id="cover_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon_size">0</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">cover-scroll</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkRevealer" id="info_revealer">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="transition-type">slide-right</property>
<child>
<object class="GtkScrolledWindow" id="cover_info_scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="vscrollbar-policy">never</property>
<property name="max-content-width">200</property>
<property name="propagate-natural-width">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<child>
<!-- n-columns=3 n-rows=3 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-bottom">5</property>
<property name="row-spacing">5</property>
<property name="column-homogeneous">True</property>
<child>
<object class="GtkLabel" id="album_title_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label">Album</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="album_date_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label">Date</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="album_artist_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label">Artist</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<style>
<class name="cover-labels"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">10</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="songs_scale">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="orientation">vertical</property>
<property name="restrict-to-fill-level">False</property>
<property name="digits">0</property>
<property name="draw-value">False</property>
<signal name="button-press-event" handler="on_songs_start_change" swapped="no"/>
<signal name="button-release-event" handler="on_songs_change" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="sidebar"/>
<class name="background"/>
</style>
</object>
</child>
</template>
</interface>

33
data/ui/cover-toolbar.ui Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<template class="McgCoverToolbar" parent="GtkButtonBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="fullscreen_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Show the cover in fullscreen mode</property>
<property name="action-name">win.toggle-fullscreen</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-fullscreen-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="non-homogeneous">True</property>
</packing>
</child>
</template>
</interface>

View file

@ -17,6 +17,16 @@
<attribute name="label" translatable="yes">Clear Playlist</attribute>
<attribute name="accel">&lt;Primary&gt;r</attribute>
</item>
<item>
<attribute name="action">win.toggle-fullscreen</attribute>
<attribute name="label" translatable="yes">Toggle Fullscreen</attribute>
<attribute name="accel">F11</attribute>
</item>
<item>
<attribute name="action">win.search-library</attribute>
<attribute name="label" translatable="yes">Search Library</attribute>
<attribute name="accel">&lt;Primary&gt;f</attribute>
</item>
</section>
<section>
<item>
@ -46,7 +56,7 @@
</section>
<section>
<item>
<attribute name="action">app.shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
<attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
<attribute name="accel">&lt;Primary&gt;k</attribute>
</item>

46
data/ui/info-dialog.ui Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<template class="McgInfoDialog" parent="GtkAboutDialog">
<property name="can-focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window-position">center</property>
<property name="type-hint">dialog</property>
<property name="gravity">center</property>
<property name="program-name">CoverGrid</property>
<property name="version">3.2.1</property>
<property name="comments" translatable="yes">CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.</property>
<property name="website">http://www.suruatoel.xyz/codes/mcg</property>
<property name="logo-icon-name">xyz.suruatoel.mcg</property>
<property name="license-type">gpl-3-0</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</template>
</interface>

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

@ -0,0 +1,303 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.16"/>
<template class="McgLibraryPanel" parent="GtkStack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">slide-left-right</property>
<child>
<object class="GtkBox" id="panel_normal">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchBar" id="filter_bar">
<property name="visible">True</property>
<property name="app-paintable">True</property>
<property name="can-focus">False</property>
<signal name="notify" handler="on_filter_bar_notify" swapped="no"/>
<child>
<object class="GtkSearchEntry" id="filter_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">search library</property>
<signal name="search-changed" handler="on_filter_entry_changed" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkBox" id="progress_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage" id="progress_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkProgressBar" id="progress_bar">
<property name="width-request">200</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pulse-step">0</property>
<property name="show-text">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">page1</property>
<property name="title">page1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkIconView" id="library_grid">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin">0</property>
<property name="item-orientation">horizontal</property>
<property name="row-spacing">0</property>
<property name="column-spacing">0</property>
<property name="tooltip-column">1</property>
<property name="item-padding">0</property>
<property name="activate-on-single-click">True</property>
<signal name="item-activated" handler="on_library_grid_clicked" swapped="no"/>
<signal name="selection-changed" handler="on_library_grid_selection_changed" swapped="no"/>
<signal name="size-allocate" handler="on_resize" swapped="no"/>
<style>
<class name="no-bg"/>
</style>
</object>
</child>
<style>
<class name="no-bg"/>
</style>
</object>
<packing>
<property name="name">page0</property>
<property name="title">page0</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRevealer" id="actionbar_revealer">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">none</property>
<child>
<object class="GtkActionBar" id="library-actionbar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">queue</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_add_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">page0</property>
<property name="title">page0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="panel_standalone">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkStack" id="standalone_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<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>
<packing>
<property name="name">standalone-spinne</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="standalone_scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="kinetic-scrolling">False</property>
<property name="overlay-scrolling">False</property>
<signal name="size-allocate" handler="on_standalone_scroll_size_allocate" swapped="no"/>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImage" id="standalone_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">gtk-missing-image</property>
<property name="icon_size">6</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">standalone-scroll</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkActionBar" id="library-standalone-actionbar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">queue</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_queue_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">play</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">page1</property>
<property name="title">page1</property>
<property name="position">1</property>
</packing>
</child>
</template>
</interface>

248
data/ui/library-toolbar.ui Normal file
View file

@ -0,0 +1,248 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="grid_adjustment">
<property name="lower">100</property>
<property name="upper">1000</property>
<property name="value">150</property>
<property name="step-increment">1</property>
<property name="page-increment">10</property>
</object>
<object class="GtkPopover" id="toolbar_popover">
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScale" id="grid_scale">
<property name="width-request">350</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="adjustment">grid_adjustment</property>
<property name="restrict-to-fill-level">False</property>
<property name="fill-level">-1</property>
<property name="round-digits">0</property>
<property name="digits">0</property>
<property name="has-origin">False</property>
<signal name="button-release-event" handler="on_grid_scale_changed" swapped="no"/>
<signal name="change-value" handler="on_grid_scale_change" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="library-toolbar-update">
<property name="label">gtk-refresh</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="relief">none</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_update_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="library-toolbar-sort">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Sort</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="sort_artist">
<property name="label" translatable="yes">sort by artist</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">sort_year</property>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="sort_title">
<property name="label" translatable="yes">sort by title</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">sort_year</property>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="sort_year">
<property name="label" translatable="yes">sort by year</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="sort_modified">
<property name="label" translatable="yes">sort by modification</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">sort_year</property>
<signal name="toggled" handler="on_sort_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="toolbar_sort_order_button">
<property name="label">gtk-sort-descending</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
<signal name="toggled" handler="on_sort_order_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<template class="McgLibraryToolbar" parent="GtkButtonBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
<child>
<object class="GtkToggleButton" id="toolbar_search_bar">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Search the library</property>
<signal name="toggled" handler="on_search_toggled" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">system-search-symbolic</property>
</object>
</child>
<accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="non-homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="select_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Select multiple albums</property>
<signal name="toggled" handler="on_select_toggled" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
<property name="non-homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Settings and actions</property>
<property name="popover">toolbar_popover</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">open-menu-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
<property name="non-homogeneous">True</property>
</packing>
</child>
</template>
</interface>

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

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.16"/>
<template class="McgPlaylistPanel" parent="GtkStack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">slide-left-right</property>
<child>
<object class="GtkBox" id="panel_normal">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkIconView" id="playlist_grid">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin">0</property>
<property name="item-orientation">horizontal</property>
<property name="row-spacing">0</property>
<property name="column-spacing">0</property>
<property name="tooltip-column">1</property>
<property name="item-padding">0</property>
<property name="activate-on-single-click">True</property>
<signal name="item-activated" handler="on_playlist_grid_clicked" swapped="no"/>
<signal name="selection-changed" handler="on_playlist_grid_selection_changed" swapped="no"/>
<style>
<class name="no-bg"/>
</style>
</object>
</child>
<style>
<class name="no-bg"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRevealer" id="actionbar_revealer">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-type">slide-up</property>
<child>
<object class="GtkActionBar" id="actionbar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_cancel_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">remove</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_selection_remove_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">page2</property>
<property name="title">page2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="panel_standalone">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkStack" id="standalone_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkSpinner" id="standalone_spinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="name">standalone-spinne</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="standalone_scroll">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="kinetic-scrolling">False</property>
<property name="overlay-scrolling">False</property>
<signal name="size-allocate" handler="on_standalone_scroll_size_allocate" swapped="no"/>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImage" id="standalone_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">gtk-missing-image</property>
<property name="icon_size">6</property>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">standalone-scroll</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkActionBar" id="actionbar_standalone">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">remove</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_remove_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">play</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_standalone_play_clicked" swapped="no"/>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">page1</property>
<property name="title">page1</property>
<property name="position">1</property>
</packing>
</child>
</template>
</interface>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<template class="McgPlaylistToolbar" parent="GtkButtonBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
<child>
<object class="GtkToggleButton" id="select_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Select multiple albums</property>
<signal name="toggled" handler="on_select_toggled" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">object-select-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
<property name="non-homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="playlist_clear_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Clear the playlist</property>
<signal name="clicked" handler="on_clear_clicked" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
<property name="non-homogeneous">True</property>
</packing>
</child>
</template>
</interface>

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

@ -0,0 +1,547 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.16"/>
<template class="McgServerPanel" parent="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkStackSidebar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stack">stack</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">dialog-information-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<!-- n-columns=3 n-rows=4 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="row-spacing">2</property>
<property name="column-spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">File:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Audio:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Bitrate:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Error:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="status_bitrate">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">&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>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="status_audio">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">&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>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="status_file">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">&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>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="status_error">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">&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>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">Status</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">starred-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<!-- n-columns=3 n-rows=7 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="row-spacing">2</property>
<property name="column-spacing">5</property>
<child>
<object class="GtkLabel" id="stats_artists">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="justify">right</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="stats_albums">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="stats_songs">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Albums</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Songs</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Artists</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="stats_dbplaytime">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Seconds</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="stats_uptime">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">6</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="stats_playtime">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Seconds played</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Seconds running</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">6</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">page1</property>
<property name="title" translatable="yes">Statistics</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child type="center">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">audio-speakers-symbolic</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkListBox" id="output_devices">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="selection-mode">none</property>
<style>
<class name="no-bg"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">page2</property>
<property name="title" translatable="yes">Audio Devices</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</template>
</interface>

14
data/ui/server-toolbar.ui Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<template class="McgServerToolbar" parent="GtkButtonBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="layout-style">start</property>
<child>
<placeholder/>
</child>
</template>
</interface>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.18"/>
<object class="GtkShortcutsWindow" id="shortcuts-dialog">
<template class="McgShortcutsDialog" parent="GtkShortcutsWindow">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
@ -97,7 +98,7 @@
<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>
@ -118,5 +119,5 @@
</child>
</object>
</child>
</object>
</template>
</interface>

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

@ -0,0 +1,229 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="McgAppWindow" parent="GtkApplicationWindow">
<property name="can-focus">False</property>
<property name="icon-name">xyz.suruatoel.mcg</property>
<signal name="size-allocate" handler="on_resize" swapped="no"/>
<signal name="window-state-event" handler="on_state" swapped="no"/>
<child>
<object class="GtkOverlay">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkStack" id="content_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-duration">100</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStack" id="panel_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="name">page0</property>
<property name="title">page0</property>
</packing>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkRevealer" id="info_revealer">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">start</property>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can-focus">False</property>
<property name="valign">start</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="orientation">vertical</property>
<property name="baseline-position">top</property>
<property name="show-close-button">True</property>
<signal name="close" handler="on_info_bar_close" swapped="no"/>
<signal name="response" handler="on_info_bar_response" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="info_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar" id="headerbar">
<property name="name">headerbar</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<child type="title">
<object class="GtkStack" id="headerbar_title_stack">
<property name="name">headerbar-connection</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="transition-duration">100</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkLabel" id="headerbar_connection_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Connect to MPD</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="name">page1</property>
<property name="title">page1</property>
</packing>
</child>
<child>
<object class="GtkStackSwitcher" id="headerbar_panel_switcher">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stack">panel_stack</property>
</object>
<packing>
<property name="name">page0</property>
<property name="title">page0</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="headerbar_button_connect">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="tooltip-text" translatable="yes">Connect or disconnect</property>
<signal name="notify::active" handler="on_headerbar_connection_active_notify" swapped="no"/>
<signal name="state-set" handler="on_headerbar_connection_state_set" swapped="no"/>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="headerbar_button_playpause">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Switch between play and pause</property>
<signal name="toggled" handler="on_headerbar_playpause_toggled" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-start</property>
</object>
</child>
</object>
<packing>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkVolumeButton" id="headerbar_button_volume">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Adjust the volume</property>
<property name="relief">none</property>
<property name="orientation">vertical</property>
<signal name="button-press-event" handler="on_headerbar_volume_press" swapped="no"/>
<signal name="button-release-event" handler="on_headerbar_volume_release" swapped="no"/>
<signal name="value-changed" handler="on_headerbar_volume_changed" swapped="no"/>
<child internal-child="plus_button">
<object class="GtkButton">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
</object>
</child>
<child internal-child="minus_button">
<object class="GtkButton">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
</object>
</child>
</object>
<packing>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkStack" id="toolbar_stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">4</property>
</packing>
</child>
</object>
</child>
<style>
<class name="bg-texture"/>
</style>
</template>
</interface>

View file

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

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

Binary file not shown.

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

19
meson.build Normal file
View file

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

1
po/LINGUAS Normal file
View file

@ -0,0 +1 @@
en de

29
po/POTFILES Normal file
View file

@ -0,0 +1,29 @@
data/xyz.suruatoel.mcg.gschema.xml
data/ui/album-headerbar.ui
data/ui/connection-panel.ui
data/ui/cover-panel.ui
data/ui/cover-toolbar.ui
data/ui/gtk.menu.ui
data/ui/info-dialog.ui
data/ui/library-panel.ui
data/ui/library-toolbar.ui
data/ui/playlist-panel.ui
data/ui/playlist-toolbar.ui
data/ui/server-panel.ui
data/ui/server-toolbar.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/infodialog.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.

411
po/de.po Normal file
View file

@ -0,0 +1,411 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-08 19:06+0100\n"
"PO-Revision-Date: 2023-01-08 19:07+0100\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.2.2\n"
"X-Poedit-Basepath: ../../..\n"
"X-Poedit-SourceCharset: UTF-8\n"
#: data/xyz.suruatoel.mcg.gschema.xml:11
msgid "MPD host"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:12
msgid "MPD host to connect to"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:16
msgid "MPD port"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:17
msgid "MPD port to connect to"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:21
#, fuzzy
msgid "Connection state"
msgstr "Verbindung"
#: data/xyz.suruatoel.mcg.gschema.xml:22
msgid "State of last connection"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:26
msgid "Window width"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:27
msgid "The window width in pixels."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:31
msgid "Window height"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:32
msgid "The window height in pixels."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:36
msgid "Window maximized"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:37
msgid "Whether or not the window is in maximized state."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:42
msgid "Last selected panel"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:43
msgid "The index of the last selected panel."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:48
msgid "Size of library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:49
msgid "The size of items displayed in the library."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:53
msgid "Sort criterium for library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:54
msgid "The sort criterium of items displayed in the library."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:58
msgid "Sort type for library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:59
msgid "The sort type of items displayed in the library."
msgstr ""
#: data/ui/album-headerbar.ui:17
msgid "Title"
msgstr "Titel"
#: data/ui/album-headerbar.ui:34
msgid "Artist"
msgstr "Künstler"
#: data/ui/connection-panel.ui:76
msgid "Enter hostname or IP address"
msgstr "Hostnamen oder IP-Adresse eingeben"
#: data/ui/connection-panel.ui:89
msgid "Enter password or leave blank"
msgstr "Passwort eingeben oder leer lassen"
#: data/ui/connection-panel.ui:118
msgid "Host:"
msgstr "Host:"
#: data/ui/connection-panel.ui:130
msgid "Port:"
msgstr "Port:"
#: data/ui/connection-panel.ui:142
msgid "Password:"
msgstr "Passwort:"
#: data/ui/cover-toolbar.ui:15 data/ui/shortcuts-dialog.ui:102
msgid "Show the cover in fullscreen mode"
msgstr "Das Cover im Vollbildmodus anzeigen"
#: data/ui/gtk.menu.ui:7
msgid "Connect"
msgstr "Verbinden"
#: data/ui/gtk.menu.ui:12
msgid "Play"
msgstr "Abspielen"
#: data/ui/gtk.menu.ui:17
msgid "Clear Playlist"
msgstr "Playlist leeren"
#: data/ui/gtk.menu.ui:22
msgid "Toggle Fullscreen"
msgstr "Vollbild wechseln"
#: data/ui/gtk.menu.ui:27
msgid "Search Library"
msgstr "Bibliothek durchsuchen"
#: data/ui/gtk.menu.ui:34
msgid "Connection"
msgstr "Verbindung"
#: data/ui/gtk.menu.ui:40 src/window.py:113
msgid "Cover"
msgstr "Cover"
#: data/ui/gtk.menu.ui:46 src/window.py:114
msgid "Playlist"
msgstr "Wiedergabeliste"
#: data/ui/gtk.menu.ui:52 src/window.py:115
msgid "Library"
msgstr "Bibliothek"
#: data/ui/gtk.menu.ui:60
msgid "Keyboard Shortcuts"
msgstr "Tastenkombinationen"
#: data/ui/gtk.menu.ui:65
msgid "Info"
msgstr "Info"
#: data/ui/gtk.menu.ui:70
msgid "Quit"
msgstr "Beenden"
#: data/ui/info-dialog.ui:14
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/ui/library-panel.ui:27
msgid "search library"
msgstr "Bibliothek durchsuchen"
#: data/ui/library-panel.ui:166 data/ui/playlist-panel.ui:57
msgid "cancel"
msgstr "abbrechen"
#: data/ui/library-panel.ui:179 data/ui/library-panel.ui:264
msgid "queue"
msgstr "einreihen"
#: data/ui/library-panel.ui:277 data/ui/playlist-panel.ui:168
msgid "play"
msgstr "abspielen"
#: data/ui/library-toolbar.ui:76
msgid "Sort"
msgstr "Sortierung"
#: data/ui/library-toolbar.ui:86
msgid "sort by artist"
msgstr "nach Künstler"
#: data/ui/library-toolbar.ui:102
msgid "sort by title"
msgstr "nach Titel"
#: data/ui/library-toolbar.ui:118
msgid "sort by year"
msgstr "nach Jahr"
#: data/ui/library-toolbar.ui:134
msgid "sort by modification"
msgstr "nach Änderungsdatum"
#: data/ui/library-toolbar.ui:185 data/ui/shortcuts-dialog.ui:115
msgid "Search the library"
msgstr "Die Bibliothek durchsuchen"
#: data/ui/library-toolbar.ui:208 data/ui/playlist-toolbar.ui:15
msgid "Select multiple albums"
msgstr "Mehrere Alben auswählen"
#: data/ui/library-toolbar.ui:230
msgid "Settings and actions"
msgstr "Einstellungen und Aktionen"
#: data/ui/playlist-panel.ui:70 data/ui/playlist-panel.ui:155
msgid "remove"
msgstr "entfernen"
#: data/ui/playlist-toolbar.ui:37 data/ui/shortcuts-dialog.ui:89
msgid "Clear the playlist"
msgstr "Die Wiedergabeliste leeren"
#: data/ui/server-panel.ui:69
msgid "File:"
msgstr "Datei:"
#: data/ui/server-panel.ui:82
msgid "Audio:"
msgstr "Audio:"
#: data/ui/server-panel.ui:95
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/ui/server-panel.ui:108
msgid "Error:"
msgstr "Fehler:"
#: data/ui/server-panel.ui:120 data/ui/server-panel.ui:136
#: data/ui/server-panel.ui:152 data/ui/server-panel.ui:168
msgid "<i>none</i>"
msgstr "<i>nichts</i>"
#: data/ui/server-panel.ui:221
msgid "Status"
msgstr "Status"
#: data/ui/server-panel.ui:301
msgid "Albums"
msgstr "Alben"
#: data/ui/server-panel.ui:313
msgid "Songs"
msgstr "Songs"
#: data/ui/server-panel.ui:325
msgid "Artists"
msgstr "Künstler"
#: data/ui/server-panel.ui:349
msgid "Seconds"
msgstr "Sekunden"
#: data/ui/server-panel.ui:396
msgid "Seconds played"
msgstr "Sekunden gespielt"
#: data/ui/server-panel.ui:407
msgid "Seconds running"
msgstr "Sekunden laufend"
#: data/ui/server-panel.ui:465
msgid "Statistics"
msgstr "Statistiken"
#: data/ui/server-panel.ui:535
msgid "Audio Devices"
msgstr "Audiogeräte"
#: data/ui/shortcuts-dialog.ui:15
msgid "General"
msgstr "Allgemein"
#: data/ui/shortcuts-dialog.ui:20
msgid "Switch to the Connection panel"
msgstr "Zum Verbindungspaneel wechseln"
#: data/ui/shortcuts-dialog.ui:27
msgid "Switch to the Cover panel"
msgstr "Zum Cover-Paneel wechseln"
#: data/ui/shortcuts-dialog.ui:34
msgid "Switch to the Playlist panel"
msgstr "Zum Wiedergabelistenpaneel wechseln"
#: data/ui/shortcuts-dialog.ui:41
msgid "Switch to the Library panel"
msgstr "Zum Bibliothekspaneel wechseln"
#: data/ui/shortcuts-dialog.ui:48
msgid "Show the keyboard shortcuts"
msgstr "Die Tastenkombinationen anzeigen (dieser Dialog)"
#: data/ui/shortcuts-dialog.ui:55
msgid "Open the info dialog"
msgstr "Den Infodialog öffnen"
#: data/ui/shortcuts-dialog.ui:62
msgid "Quit the application"
msgstr "Die Anwendung beenden"
#: data/ui/shortcuts-dialog.ui:70
msgid "Player"
msgstr "Wiedergabeprogramm"
#: data/ui/shortcuts-dialog.ui:75 data/ui/window.ui:139
msgid "Connect or disconnect"
msgstr "Die Verbindung herstellen oder trennen"
#: data/ui/shortcuts-dialog.ui:82 data/ui/window.ui:161
msgid "Switch between play and pause"
msgstr "Zwischen Abspielen und Pause wechseln"
#: data/ui/shortcuts-dialog.ui:97
msgid "Cover Panel"
msgstr "Cover-Paneel"
#: data/ui/shortcuts-dialog.ui:110
msgid "Library Panel"
msgstr "Bibliothekspaneel"
#: data/ui/window.ui:108
msgid "Connect to MPD"
msgstr "Zu MPD verbinden"
#: data/ui/window.ui:181
msgid "Adjust the volume"
msgstr "Die Lautstärke anpassen"
#: src/librarypanel.py:421
msgid "Loading albums"
msgstr "Alben werden geladen"
#: src/librarypanel.py:521
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:112
msgid "Server"
msgstr "Server"
#~ 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.

412
po/en.po Normal file
View file

@ -0,0 +1,412 @@
msgid ""
msgstr ""
"Project-Id-Version: CoverGrid (mcg)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-08 19:06+0100\n"
"PO-Revision-Date: 2023-01-08 19:07+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"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.2.2\n"
"X-Poedit-Basepath: ../../..\n"
"X-Poedit-SearchPath-0: mcg\n"
"X-Poedit-SearchPath-1: data/ui\n"
#: data/xyz.suruatoel.mcg.gschema.xml:11
msgid "MPD host"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:12
msgid "MPD host to connect to"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:16
msgid "MPD port"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:17
msgid "MPD port to connect to"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:21
#, fuzzy
msgid "Connection state"
msgstr "Connection"
#: data/xyz.suruatoel.mcg.gschema.xml:22
msgid "State of last connection"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:26
msgid "Window width"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:27
msgid "The window width in pixels."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:31
msgid "Window height"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:32
msgid "The window height in pixels."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:36
msgid "Window maximized"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:37
msgid "Whether or not the window is in maximized state."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:42
msgid "Last selected panel"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:43
msgid "The index of the last selected panel."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:48
msgid "Size of library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:49
msgid "The size of items displayed in the library."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:53
msgid "Sort criterium for library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:54
msgid "The sort criterium of items displayed in the library."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:58
msgid "Sort type for library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:59
msgid "The sort type of items displayed in the library."
msgstr ""
#: data/ui/album-headerbar.ui:17
msgid "Title"
msgstr "Title"
#: data/ui/album-headerbar.ui:34
msgid "Artist"
msgstr "Artist"
#: data/ui/connection-panel.ui:76
msgid "Enter hostname or IP address"
msgstr "Enter hostname or IP address"
#: data/ui/connection-panel.ui:89
msgid "Enter password or leave blank"
msgstr "Enter password or leave blank"
#: data/ui/connection-panel.ui:118
msgid "Host:"
msgstr "Host:"
#: data/ui/connection-panel.ui:130
msgid "Port:"
msgstr "Port:"
#: data/ui/connection-panel.ui:142
msgid "Password:"
msgstr "Password:"
#: data/ui/cover-toolbar.ui:15 data/ui/shortcuts-dialog.ui:102
msgid "Show the cover in fullscreen mode"
msgstr "Show the cover in fullscreen mode"
#: data/ui/gtk.menu.ui:7
msgid "Connect"
msgstr "Connect"
#: data/ui/gtk.menu.ui:12
msgid "Play"
msgstr "Play"
#: data/ui/gtk.menu.ui:17
msgid "Clear Playlist"
msgstr "Clear Playlist"
#: data/ui/gtk.menu.ui:22
msgid "Toggle Fullscreen"
msgstr "Toggle fullscreen"
#: data/ui/gtk.menu.ui:27
msgid "Search Library"
msgstr "Search Library"
#: data/ui/gtk.menu.ui:34
msgid "Connection"
msgstr "Connection"
#: data/ui/gtk.menu.ui:40 src/window.py:113
msgid "Cover"
msgstr "Cover"
#: data/ui/gtk.menu.ui:46 src/window.py:114
msgid "Playlist"
msgstr "Playlist"
#: data/ui/gtk.menu.ui:52 src/window.py:115
msgid "Library"
msgstr "Library"
#: data/ui/gtk.menu.ui:60
msgid "Keyboard Shortcuts"
msgstr "Keyboard Shortcuts"
#: data/ui/gtk.menu.ui:65
msgid "Info"
msgstr "Info"
#: data/ui/gtk.menu.ui:70
msgid "Quit"
msgstr "Quit"
#: data/ui/info-dialog.ui:14
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/ui/library-panel.ui:27
msgid "search library"
msgstr "search library"
#: data/ui/library-panel.ui:166 data/ui/playlist-panel.ui:57
msgid "cancel"
msgstr "cancel"
#: data/ui/library-panel.ui:179 data/ui/library-panel.ui:264
msgid "queue"
msgstr "queue"
#: data/ui/library-panel.ui:277 data/ui/playlist-panel.ui:168
msgid "play"
msgstr "play"
#: data/ui/library-toolbar.ui:76
msgid "Sort"
msgstr "Sort order"
#: data/ui/library-toolbar.ui:86
msgid "sort by artist"
msgstr "by Artist"
#: data/ui/library-toolbar.ui:102
msgid "sort by title"
msgstr "by Title"
#: data/ui/library-toolbar.ui:118
msgid "sort by year"
msgstr "by Year"
#: data/ui/library-toolbar.ui:134
msgid "sort by modification"
msgstr "by Modification Date"
#: data/ui/library-toolbar.ui:185 data/ui/shortcuts-dialog.ui:115
msgid "Search the library"
msgstr "Search the library"
#: data/ui/library-toolbar.ui:208 data/ui/playlist-toolbar.ui:15
msgid "Select multiple albums"
msgstr "Select multiple albums"
#: data/ui/library-toolbar.ui:230
msgid "Settings and actions"
msgstr "Settings and actions"
#: data/ui/playlist-panel.ui:70 data/ui/playlist-panel.ui:155
msgid "remove"
msgstr "remove"
#: data/ui/playlist-toolbar.ui:37 data/ui/shortcuts-dialog.ui:89
msgid "Clear the playlist"
msgstr "Clear the playlist"
#: data/ui/server-panel.ui:69
msgid "File:"
msgstr "File:"
#: data/ui/server-panel.ui:82
msgid "Audio:"
msgstr "Audio:"
#: data/ui/server-panel.ui:95
msgid "Bitrate:"
msgstr "Bitrate:"
#: data/ui/server-panel.ui:108
msgid "Error:"
msgstr "Error:"
#: data/ui/server-panel.ui:120 data/ui/server-panel.ui:136
#: data/ui/server-panel.ui:152 data/ui/server-panel.ui:168
msgid "<i>none</i>"
msgstr "<i>none</i>"
#: data/ui/server-panel.ui:221
msgid "Status"
msgstr "Status"
#: data/ui/server-panel.ui:301
msgid "Albums"
msgstr "Albums"
#: data/ui/server-panel.ui:313
msgid "Songs"
msgstr "Songs"
#: data/ui/server-panel.ui:325
msgid "Artists"
msgstr "Artists"
#: data/ui/server-panel.ui:349
msgid "Seconds"
msgstr "Seconds"
#: data/ui/server-panel.ui:396
msgid "Seconds played"
msgstr "Seconds"
#: data/ui/server-panel.ui:407
msgid "Seconds running"
msgstr "Seconds running"
#: data/ui/server-panel.ui:465
msgid "Statistics"
msgstr "Statistics"
#: data/ui/server-panel.ui:535
msgid "Audio Devices"
msgstr "Audio Devices"
#: data/ui/shortcuts-dialog.ui:15
msgid "General"
msgstr "General"
#: data/ui/shortcuts-dialog.ui:20
msgid "Switch to the Connection panel"
msgstr "Switch to the Connection panel"
#: data/ui/shortcuts-dialog.ui:27
msgid "Switch to the Cover panel"
msgstr "Switch to the Cover panel"
#: data/ui/shortcuts-dialog.ui:34
msgid "Switch to the Playlist panel"
msgstr "Switch to the Playlist panel"
#: data/ui/shortcuts-dialog.ui:41
msgid "Switch to the Library panel"
msgstr "Switch to the Cover panel"
#: data/ui/shortcuts-dialog.ui:48
msgid "Show the keyboard shortcuts"
msgstr "Show the keyboard shortcuts (this dialog)"
#: data/ui/shortcuts-dialog.ui:55
msgid "Open the info dialog"
msgstr "Open the info dialog"
#: data/ui/shortcuts-dialog.ui:62
msgid "Quit the application"
msgstr "Quit the application"
#: data/ui/shortcuts-dialog.ui:70
msgid "Player"
msgstr "Player"
#: data/ui/shortcuts-dialog.ui:75 data/ui/window.ui:139
msgid "Connect or disconnect"
msgstr "Connect or disconnect"
#: data/ui/shortcuts-dialog.ui:82 data/ui/window.ui:161
msgid "Switch between play and pause"
msgstr "Switch between play and pause"
#: data/ui/shortcuts-dialog.ui:97
msgid "Cover Panel"
msgstr "Cover Panel"
#: data/ui/shortcuts-dialog.ui:110
msgid "Library Panel"
msgstr "Library Panel"
#: data/ui/window.ui:108
msgid "Connect to MPD"
msgstr "Connect to MPD"
#: data/ui/window.ui:181
msgid "Adjust the volume"
msgstr "Adjust the volume"
#: src/librarypanel.py:421
msgid "Loading albums"
msgstr "Loading albums"
#: src/librarypanel.py:521
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:112
msgid "Server"
msgstr "Server"
#~ 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"

377
po/mcg.pot Normal file
View file

@ -0,0 +1,377 @@
# 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: 2023-01-08 19:06+0100\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/xyz.suruatoel.mcg.gschema.xml:11
msgid "MPD host"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:12
msgid "MPD host to connect to"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:16
msgid "MPD port"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:17
msgid "MPD port to connect to"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:21
msgid "Connection state"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:22
msgid "State of last connection"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:26
msgid "Window width"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:27
msgid "The window width in pixels."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:31
msgid "Window height"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:32
msgid "The window height in pixels."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:36
msgid "Window maximized"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:37
msgid "Whether or not the window is in maximized state."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:42
msgid "Last selected panel"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:43
msgid "The index of the last selected panel."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:48
msgid "Size of library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:49
msgid "The size of items displayed in the library."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:53
msgid "Sort criterium for library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:54
msgid "The sort criterium of items displayed in the library."
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:58
msgid "Sort type for library items"
msgstr ""
#: data/xyz.suruatoel.mcg.gschema.xml:59
msgid "The sort type of items displayed in the library."
msgstr ""
#: data/ui/album-headerbar.ui:17
msgid "Title"
msgstr ""
#: data/ui/album-headerbar.ui:34
msgid "Artist"
msgstr ""
#: data/ui/connection-panel.ui:76
msgid "Enter hostname or IP address"
msgstr ""
#: data/ui/connection-panel.ui:89
msgid "Enter password or leave blank"
msgstr ""
#: data/ui/connection-panel.ui:118
msgid "Host:"
msgstr ""
#: data/ui/connection-panel.ui:130
msgid "Port:"
msgstr ""
#: data/ui/connection-panel.ui:142
msgid "Password:"
msgstr ""
#: data/ui/cover-toolbar.ui:15 data/ui/shortcuts-dialog.ui:102
msgid "Show the cover in fullscreen mode"
msgstr ""
#: data/ui/gtk.menu.ui:7
msgid "Connect"
msgstr ""
#: data/ui/gtk.menu.ui:12
msgid "Play"
msgstr ""
#: data/ui/gtk.menu.ui:17
msgid "Clear Playlist"
msgstr ""
#: data/ui/gtk.menu.ui:22
msgid "Toggle Fullscreen"
msgstr ""
#: data/ui/gtk.menu.ui:27
msgid "Search Library"
msgstr ""
#: data/ui/gtk.menu.ui:34
msgid "Connection"
msgstr ""
#: data/ui/gtk.menu.ui:40 src/window.py:113
msgid "Cover"
msgstr ""
#: data/ui/gtk.menu.ui:46 src/window.py:114
msgid "Playlist"
msgstr ""
#: data/ui/gtk.menu.ui:52 src/window.py:115
msgid "Library"
msgstr ""
#: data/ui/gtk.menu.ui:60
msgid "Keyboard Shortcuts"
msgstr ""
#: data/ui/gtk.menu.ui:65
msgid "Info"
msgstr ""
#: data/ui/gtk.menu.ui:70
msgid "Quit"
msgstr ""
#: data/ui/info-dialog.ui:14
msgid ""
"CoverGrid is a client for the Music Player Daemon, focusing on albums "
"instead of single tracks."
msgstr ""
#: data/ui/library-panel.ui:27
msgid "search library"
msgstr ""
#: data/ui/library-panel.ui:166 data/ui/playlist-panel.ui:57
msgid "cancel"
msgstr ""
#: data/ui/library-panel.ui:179 data/ui/library-panel.ui:264
msgid "queue"
msgstr ""
#: data/ui/library-panel.ui:277 data/ui/playlist-panel.ui:168
msgid "play"
msgstr ""
#: data/ui/library-toolbar.ui:76
msgid "Sort"
msgstr ""
#: data/ui/library-toolbar.ui:86
msgid "sort by artist"
msgstr ""
#: data/ui/library-toolbar.ui:102
msgid "sort by title"
msgstr ""
#: data/ui/library-toolbar.ui:118
msgid "sort by year"
msgstr ""
#: data/ui/library-toolbar.ui:134
msgid "sort by modification"
msgstr ""
#: data/ui/library-toolbar.ui:185 data/ui/shortcuts-dialog.ui:115
msgid "Search the library"
msgstr ""
#: data/ui/library-toolbar.ui:208 data/ui/playlist-toolbar.ui:15
msgid "Select multiple albums"
msgstr ""
#: data/ui/library-toolbar.ui:230
msgid "Settings and actions"
msgstr ""
#: data/ui/playlist-panel.ui:70 data/ui/playlist-panel.ui:155
msgid "remove"
msgstr ""
#: data/ui/playlist-toolbar.ui:37 data/ui/shortcuts-dialog.ui:89
msgid "Clear the playlist"
msgstr ""
#: data/ui/server-panel.ui:69
msgid "File:"
msgstr ""
#: data/ui/server-panel.ui:82
msgid "Audio:"
msgstr ""
#: data/ui/server-panel.ui:95
msgid "Bitrate:"
msgstr ""
#: data/ui/server-panel.ui:108
msgid "Error:"
msgstr ""
#: data/ui/server-panel.ui:120 data/ui/server-panel.ui:136
#: data/ui/server-panel.ui:152 data/ui/server-panel.ui:168
msgid "<i>none</i>"
msgstr ""
#: data/ui/server-panel.ui:221
msgid "Status"
msgstr ""
#: data/ui/server-panel.ui:301
msgid "Albums"
msgstr ""
#: data/ui/server-panel.ui:313
msgid "Songs"
msgstr ""
#: data/ui/server-panel.ui:325
msgid "Artists"
msgstr ""
#: data/ui/server-panel.ui:349
msgid "Seconds"
msgstr ""
#: data/ui/server-panel.ui:396
msgid "Seconds played"
msgstr ""
#: data/ui/server-panel.ui:407
msgid "Seconds running"
msgstr ""
#: data/ui/server-panel.ui:465
msgid "Statistics"
msgstr ""
#: data/ui/server-panel.ui:535
msgid "Audio Devices"
msgstr ""
#: data/ui/shortcuts-dialog.ui:15
msgid "General"
msgstr ""
#: data/ui/shortcuts-dialog.ui:20
msgid "Switch to the Connection panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:27
msgid "Switch to the Cover panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:34
msgid "Switch to the Playlist panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:41
msgid "Switch to the Library panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:48
msgid "Show the keyboard shortcuts"
msgstr ""
#: data/ui/shortcuts-dialog.ui:55
msgid "Open the info dialog"
msgstr ""
#: data/ui/shortcuts-dialog.ui:62
msgid "Quit the application"
msgstr ""
#: data/ui/shortcuts-dialog.ui:70
msgid "Player"
msgstr ""
#: data/ui/shortcuts-dialog.ui:75 data/ui/window.ui:139
msgid "Connect or disconnect"
msgstr ""
#: data/ui/shortcuts-dialog.ui:82 data/ui/window.ui:161
msgid "Switch between play and pause"
msgstr ""
#: data/ui/shortcuts-dialog.ui:97
msgid "Cover Panel"
msgstr ""
#: data/ui/shortcuts-dialog.ui:110
msgid "Library Panel"
msgstr ""
#: data/ui/window.ui:108
msgid "Connect to MPD"
msgstr ""
#: data/ui/window.ui:181
msgid "Adjust the volume"
msgstr ""
#: src/librarypanel.py:421
msgid "Loading albums"
msgstr ""
#: src/librarypanel.py:521
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:112
msgid "Server"
msgstr ""

3
po/meson.build Normal file
View file

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

115
setup.py
View file

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

35
src/albumheaderbar.py Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/album-headerbar.ui')
class AlbumHeaderbar(Gtk.HeaderBar):
__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()))

104
src/application.py Normal file
View file

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

108
src/connectionpanel.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
from mcg.zeroconf import ZeroconfProvider
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/connection-panel.ui')
class ConnectionPanel(Gtk.Box):
__gtype_name__ = 'McgConnectionPanel'
__gsignals__ = {
'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str))
}
# Widgets
zeroconf_list = Gtk.Template.Child()
host_entry = Gtk.Template.Child()
port_spinner = Gtk.Template.Child()
password_entry = Gtk.Template.Child()
def __init__(self):
super().__init__()
self._services = Gtk.ListStore(str, str, int)
self._profile = None
# Zeroconf
self.zeroconf_list.set_model(self._services)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0)
self.zeroconf_list.append_column(column)
# 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
self._services.append([name, host, port])
@Gtk.Template.Callback()
def on_service_selected(self, selection):
model, treeiter = selection.get_selected()
if treeiter != None:
service = model[treeiter]
self.set_host(service[1])
self.set_port(service[2])
@Gtk.Template.Callback()
def on_zeroconf_list_outfocused(self, widget, event):
self.zeroconf_list.get_selection().unselect_all()
@Gtk.Template.Callback()
def on_host_entry_outfocused(self, widget, event):
self._call_back()
@Gtk.Template.Callback()
def on_port_spinner_value_changed(self, widget):
self._call_back()
@Gtk.Template.Callback()
def on_password_entry_outfocused(self, widget, event):
self._call_back()
def set_host(self, host):
self.host_entry.set_text(host)
def get_host(self):
return self.host_entry.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_entry.set_text(password)
def get_password(self):
if self.password_entry.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(),)

283
src/coverpanel.py Normal file
View file

@ -0,0 +1,283 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import logging
import math
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf
from mcg.utils import Utils
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/cover-toolbar.ui')
class CoverToolbar(Gtk.ButtonBox):
__gtype_name__ = 'McgCoverToolbar'
__gsignals__ = {
'fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ())
}
# Widgets
fullscreen_button = Gtk.Template.Child()
def __init__(self):
super().__init__()
def set_fullscreen_sensitive(self, sensitive):
self.fullscreen_button.set_sensitive(sensitive)
@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
# Cover
cover_stack = Gtk.Template.Child()
cover_spinner = 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):
super().__init__()
self._current_album = None
self._current_cover_album = None
self._cover_pixbuf = None
self._timer = None
self._properties = {}
self._icon_theme = Gtk.IconTheme.get_default()
self._fullscreened = False
self._is_selected = False
self._current_size = None
self._cover_pixbuf = self._get_default_image()
# Widgets
self._toolbar = CoverToolbar()
self.cover_stack.set_visible_child(self.cover_scroll)
# Initial actions
GObject.idle_add(self._enable_tracklist)
def get_toolbar(self):
return self._toolbar
def set_selected(self, selected):
self._is_selected = selected
@Gtk.Template.Callback()
def on_cover_box_pressed(self, widget, event):
if self._current_album and event.type == Gdk.EventType._2BUTTON_PRESS:
self.emit('toggle-fullscreen')
@Gtk.Template.Callback()
def on_cover_size_allocate(self, widget, allocation):
GObject.idle_add(self._resize_image)
self.cover_info_scroll.set_max_content_width(allocation.width // 2)
@Gtk.Template.Callback()
def on_songs_start_change(self, widget, event):
if self._timer:
GObject.source_remove(self._timer)
self._timer = None
@Gtk.Template.Callback()
def on_songs_change(self, widget, event):
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._toolbar.set_fullscreen_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)
self.cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
GObject.idle_add(self._resize_image)
self._fullscreened = True
else:
self._fullscreened = False
self.info_revealer.set_reveal_child(True)
self.cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
GObject.idle_add(self._resize_image)
def set_albumart(self, album, data):
if album == self._current_album:
if data:
# Load image and draw it
try:
self._cover_pixbuf = Utils.load_pixbuf(data)
except Exception as e:
self._logger.exception("Failed to set albumart")
self._cover_pixbuf = self._get_default_image()
else:
# Reset image
self._cover_pixbuf = self._get_default_image()
self._current_size = None
self._current_cover_album = album
# Show image
GObject.idle_add(self._show_image)
def _set_tracks(self, album):
self.songs_scale.clear_marks()
self.songs_scale.set_range(0, album.get_length())
length = 0
for track in album.get_tracks():
cur_length = length
if length > 0 and length < album.get_length():
cur_length = cur_length + 1
self.songs_scale.add_mark(
cur_length,
Gtk.PositionType.RIGHT,
GObject.markup_escape_text(
Utils.create_track_title(track)
)
)
length = length + track.get_length()
self.songs_scale.add_mark(
length,
Gtk.PositionType.RIGHT,
"{0[0]:02d}:{0[1]:02d} minutes".format(divmod(length, 60))
)
def _enable_tracklist(self):
if self._current_album:
# enable
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):
self._resize_image()
self.cover_stack.set_visible_child(self.cover_scroll)
self.cover_spinner.stop()
def _resize_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
# Get size
size = self.cover_scroll.get_allocation()
# Abort if size is the same
if self._current_size and size.width == self._current_size.width and size.height == self._current_size.height:
return
self._current_size = size
# Get pixelbuffer
pixbuf = self._cover_pixbuf
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size.width) / float(pixbuf.get_width())
ratioH = float(size.height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self.cover_image.set_allocation(self.cover_scroll.get_allocation())
self.cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self.cover_image.show()
def _get_default_image(self):
return self._icon_theme.load_icon(
Utils.STOCK_ICON_DEFAULT,
512,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)

15
src/infodialog.py Normal file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import logging
from gi.repository import Gtk, GdkPixbuf
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/info-dialog.ui')
class InfoDialog(Gtk.AboutDialog):
__gtype_name__ = 'McgInfoDialog'

655
src/librarypanel.py Normal file
View file

@ -0,0 +1,655 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import locale
import logging
import math
import threading
from gi.repository import Gtk, GObject, GdkPixbuf
from mcg import client
from mcg.albumheaderbar import AlbumHeaderbar
from mcg.utils import SortOrder
from mcg.utils import Utils
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/library-toolbar.ui')
class LibraryToolbar(Gtk.ButtonBox):
__gtype_name__ = 'McgLibraryToolbar'
__gsignals__ = {
'select': (GObject.SIGNAL_RUN_FIRST, None, (bool,)),
'toggle-search': (GObject.SIGNAL_RUN_FIRST, None, (bool,)),
'update': (GObject.SIGNAL_RUN_FIRST, None, ()),
'start-scale': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'end-scale': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'sort': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'sort-type': (GObject.SIGNAL_RUN_FIRST, None, (Gtk.SortType,))
}
# Widgets
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()
def __init__(self, item_size):
super().__init__()
# Toolbar menu
self.grid_scale.set_value(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
}
@Gtk.Template.Callback()
def on_select_toggled(self, widget):
self.emit('select', widget.get_active())
@Gtk.Template.Callback()
def on_search_toggled(self, widget):
self.emit('toggle-search', widget.get_active())
@Gtk.Template.Callback()
def on_update_clicked(self, widget):
self.emit('update')
@Gtk.Template.Callback()
def on_grid_scale_change(self, widget, scroll, value):
self.emit('start-scale', value)
@Gtk.Template.Callback()
def on_grid_scale_changed(self, widget, event):
self.emit('end-scale', self.grid_scale.get_value())
self.toolbar_popover.popdown()
@Gtk.Template.Callback()
def on_sort_toggled(self, widget):
if widget.get_active():
sort = [key for key, value in self._toolbar_sort_buttons.items() if value is widget][0]
self.emit('sort', sort)
@Gtk.Template.Callback()
def on_sort_order_toggled(self, button):
if button.get_active():
sort_type = Gtk.SortType.DESCENDING
else:
sort_type = Gtk.SortType.ASCENDING
self.emit('sort-type', sort_type)
def get_grid_scale(self):
return self.grid_scale
def is_search_active(self):
return self.toolbar_search_bar.get_active()
def set_search_active(self, active):
self.toolbar_search_bar.set_active(active)
def exit_selection(self):
self.select_button.set_active(False)
def set_sort_order(self, sort):
button = self._toolbar_sort_buttons[sort]
if button and not button.get_active():
button.set_active(True)
def set_sort_type(self, sort_type):
if sort_type:
self.toolbar_sort_order_button.set_active(True)
else:
self.toolbar_sort_order_button.set_active(False)
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/library-panel.ui')
class LibraryPanel(Gtk.Stack):
__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, (Gtk.SortType,)),
'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,))
}
# Widgets
panel_standalone = Gtk.Template.Child()
actionbar_revealer = 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()
# 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):
super().__init__()
self._logger = logging.getLogger(__name__)
self._client = client
self._buttons = {}
self._albums = None
self._host = "localhost"
self._filter_string = ""
self._item_size = 150
self._sort_order = SortOrder.YEAR
self._sort_type = Gtk.SortType.DESCENDING
self._grid_pixbufs = {}
self._old_ranges = {}
self._library_lock = threading.Lock()
self._library_stop = threading.Event()
self._icon_theme = Gtk.IconTheme.get_default()
self._standalone_pixbuf = None
self._selected_albums = []
self._allocation = (0, 0)
self._is_selected = False
# Widgets
self._toolbar = LibraryToolbar(self._item_size)
self._toolbar.connect('select', self.on_toolbar_select)
self._toolbar.connect('toggle-search', self.on_toolbar_toggle_search)
self._toolbar.connect('update', self.on_toolbar_update)
self._toolbar.connect('start-scale', self.on_toolbar_scale)
self._toolbar.connect('end-scale', self.on_toolbar_scaled)
self._toolbar.connect('sort', self.on_toolbar_sort)
self._toolbar.connect('sort-type', self.on_toolbar_sort_type)
# Header bar
self._headerbar_standalone = AlbumHeaderbar()
self._headerbar_standalone.connect('close', self.on_standalone_close_clicked)
# Progress Bar
self.progress_image.set_from_pixbuf(self._get_default_image())
# Library Grid: Model
self._library_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
self._library_grid_model.set_sort_column_id(2, self._sort_type)
self._library_grid_filter = self._library_grid_model.filter_new()
self._library_grid_filter.set_visible_func(self.on_filter_visible)
# Library Grid
self.library_grid.set_model(self._library_grid_filter)
self.library_grid.set_pixbuf_column(0)
self.library_grid.set_text_column(-1)
self.library_grid.set_tooltip_column(1)
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_resize(self, widget, event):
new_allocation = (widget.get_allocation().width, widget.get_allocation().height)
if new_allocation == self._allocation:
return
self._allocation = new_allocation
self._toolbar.get_grid_scale().clear_marks()
width = widget.get_allocation().width
lower = int(self._toolbar.get_grid_scale().get_adjustment().get_lower())
upper = int(self._toolbar.get_grid_scale().get_adjustment().get_upper())
countMin = max(int(width / upper), 1)
countMax = max(int(width / lower), 1)
for index in range(countMin, countMax):
pixel = int(width / index)
pixel = pixel - (2 * int(pixel / 100))
self._toolbar.get_grid_scale().add_mark(
pixel,
Gtk.PositionType.BOTTOM,
None
)
def on_toolbar_toggle_search(self, widget, active):
self.filter_bar.set_search_mode(active)
def on_toolbar_select(self, widget, active):
if active:
self.actionbar_revealer.set_reveal_child(True)
self.library_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
self.library_grid.get_style_context().add_class(Utils.CSS_SELECTION)
else:
self.actionbar_revealer.set_reveal_child(False)
self.library_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.library_grid.get_style_context().remove_class(Utils.CSS_SELECTION)
def on_toolbar_update(self, widget):
self.emit('update')
def on_toolbar_scale(self, widget, value):
size = math.floor(value)
range = self._toolbar.get_grid_scale().get_adjustment()
if size < range.get_lower() or size > range.get_upper():
return
self._item_size = size
GObject.idle_add(self.library_grid.set_item_padding, size / 100)
GObject.idle_add(self._set_widget_grid_size, self.library_grid, size, True)
def on_toolbar_scaled(self, widget, value):
size = round(value)
range = self._toolbar.get_grid_scale().get_adjustment()
if size < range.get_lower() or size > range.get_upper():
return False
self.emit('item-size-changed', size)
self._redraw()
return False
def on_toolbar_sort(self, widget, sort):
self._change_sort(sort)
def on_toolbar_sort_type(self, widget, sort_type):
self._sort_type = sort_type
self._library_grid_model.set_sort_column_id(2, sort_type)
self.emit('sort-type-changed', sort_type)
@Gtk.Template.Callback()
def on_filter_bar_notify(self, widget, value):
if self._toolbar.is_search_active() is not self.filter_bar.get_search_mode():
self._toolbar.set_search_active(self.filter_bar.get_search_mode())
@Gtk.Template.Callback()
def on_filter_entry_changed(self, widget):
self._filter_string = self.filter_entry.get_text()
GObject.idle_add(self._library_grid_filter.refilter)
@Gtk.Template.Callback()
def on_library_grid_clicked(self, widget, path):
# Get selected album
path = self._library_grid_filter.convert_path_to_child_path(path)
iter = self._library_grid_model.get_iter(path)
id = self._library_grid_model.get_value(iter, 2)
album = self._albums[id]
self._selected_albums = [album]
self.emit('albumart', id)
# Show standalone album
if widget.get_selection_mode() == Gtk.SelectionMode.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_library_grid_selection_changed(self, widget):
self._selected_albums = []
for path in widget.get_selected_items():
path = self._library_grid_filter.convert_path_to_child_path(path)
iter = self._library_grid_model.get_iter(path)
id = self._library_grid_model.get_value(iter, 2)
self._selected_albums.insert(0, self._albums[id])
def on_filter_visible(self, model, iter, data):
id = model.get_value(iter, 2)
if not id in self._albums.keys():
return
album = self._albums[id]
return album.filter(self._filter_string)
@Gtk.Template.Callback()
def on_selection_cancel_clicked(self, widget):
self._toolbar.exit_selection()
@Gtk.Template.Callback()
def on_selection_add_clicked(self, widget):
ids = [album.get_id() for album in self._selected_albums]
self.emit('queue-multiple', ids)
self._toolbar.exit_selection()
@Gtk.Template.Callback()
def on_standalone_scroll_size_allocate(self, widget, allocation):
self._resize_standalone_image()
@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._toolbar.get_grid_scale().set_value(item_size)
self._redraw()
def get_item_size(self):
return self._item_size
def set_sort_order(self, sort):
if self._sort_order != sort:
self._toolbar.set_sort_order(sort)
self._sort_order = sort
self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
def get_sort_order(self):
return self._sort_order
def set_sort_type(self, sort_type):
if self._sort_type != sort_type:
self._toolbar.set_sort_type(sort_type)
if sort_type:
sort_type_gtk = Gtk.SortType.DESCENDING
else:
sort_type_gtk = Gtk.SortType.ASCENDING
if self._sort_type != sort_type_gtk:
self._sort_type = sort_type_gtk
self._library_grid_model.set_sort_column_id(2, sort_type)
def get_sort_type(self):
return (self._sort_type != Gtk.SortType.ASCENDING)
def init_albums(self):
self.progress_bar.set_text(locale.gettext("Loading albums"))
def load_albums(self):
self.progress_bar.pulse()
def set_albums(self, host, albums):
self._host = host
self._library_stop.set()
threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start()
def set_albumart(self, album, data):
if album in self._selected_albums:
if data:
# Load image and draw it
try:
self._standalone_pixbuf = Utils.load_pixbuf(data)
except Exception as e:
self._logger.exception("Failed to set albumart")
self._standalone_pixbuf = self._get_default_image()
else:
self._standalone_pixbuf = self._get_default_image()
# Show image
GObject.idle_add(self._show_image)
def compare_albums(self, model, row1, row2, criterion):
id1 = model.get_value(row1, 2)
id2 = model.get_value(row2, 2)
if not id1 or not id2:
return
return client.MCGAlbum.compare(self._albums[id1], self._albums[id2], criterion)
def stop_threads(self):
self._library_stop.set()
def _change_sort(self, sort):
self._sort_order = sort
self._library_grid_model.set_sort_func(2, self.compare_albums, sort)
self.emit('sort-order-changed', sort)
def _set_albums(self, host, albums, size):
if not self._is_selected and albums != self._albums:
GObject.idle_add(
self.get_parent().child_set_property,
self,
'needs-attention',
True
)
self._library_lock.acquire()
self._library_stop.clear()
self._albums = albums
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.library_grid.set_item_padding, size / 100)
self.library_grid.set_model(None)
self.library_grid.freeze_child_notify()
self._library_grid_model.clear()
i = 0
n = len(albums)
cache = client.MCGCache(host, size)
self._grid_pixbufs.clear()
for album_id in albums.keys():
album = albums[album_id]
pixbuf = None
try:
pixbuf = Utils.load_thumbnail(cache, self._client, album, size)
except client.CommandException:
# Exception is handled by client
pass
except Exception as e:
self._logger.exception("Failed to load albumart")
if pixbuf is None:
pixbuf = self._icon_theme.load_icon(
Utils.STOCK_ICON_DEFAULT,
self._item_size,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)
if pixbuf is not None:
self._grid_pixbufs[album.get_id()] = pixbuf
self._library_grid_model.append([
pixbuf,
GObject.markup_escape_text("\n".join([
album.get_title(),
', '.join(album.get_dates()),
Utils.create_artists_label(album),
Utils.create_length_label(album)
])),
album_id
])
i += 1
GObject.idle_add(self.progress_bar.set_fraction, i/n)
GObject.idle_add(self.progress_bar.set_text, locale.gettext("Loading images"))
if self._library_stop.is_set():
self._library_lock.release()
return
self.library_grid.set_model(self._library_grid_filter)
self.library_grid.thaw_child_notify()
self.library_grid.set_item_width(-1)
self._library_lock.release()
self.stack.set_visible_child(self.scroll)
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()
grid_filter = grid_widget.get_model()
grid_model = grid_filter.get_model()
# get old_range
grid_widget_id = id(grid_widget)
if grid_widget_id not in self._old_ranges or self._old_ranges[grid_widget_id] is None:
self._old_ranges[grid_widget_id] = range(0, len(grid_filter))
old_range = self._old_ranges[grid_widget_id]
old_start = len(old_range) > 0 and old_range[0] or 0
old_end = len(old_range) > 0 and old_range[len(old_range)-1] + 1 or 0
# calculate visible range
w = (grid_widget.get_allocation().width // size) + (vertical and 0 or 1)
h = (grid_widget.get_allocation().height // size) + (vertical and 1 or 0)
c = w * h
vis_range = grid_widget.get_visible_range()
if vis_range is None:
self._library_lock.release()
return
(vis_start,), (vis_end,) = vis_range
vis_end = min(vis_start + c, len(grid_filter))
vis_range = range(vis_start, vis_end)
# set pixbuf
cur_start = min(old_start, vis_start)
cur_end = max(old_end, vis_end)
cur_range = range(cur_start, cur_end)
for index in cur_range:
iter = grid_filter.convert_iter_to_child_iter(grid_filter[index].iter)
if index in vis_range:
album_id = grid_model.get_value(iter, 2)
pixbuf = self._grid_pixbufs[album_id]
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
else:
pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 1, 1)
grid_model.set_value(iter, 0, pixbuf)
if self._library_stop.is_set():
self._library_lock.release()
return
self._old_ranges[grid_widget_id] = vis_range
grid_widget.set_item_width(size)
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 _open_standalone(self):
self.set_visible_child(self.panel_standalone)
self.emit('open-standalone')
def _close_standalone(self):
self.set_visible_child(self.get_children()[0])
self.emit('close-standalone')
def _resize_standalone_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
pixbuf = self._standalone_pixbuf
size = self.standalone_scroll.get_allocation()
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size.width) / float(pixbuf.get_width())
ratioH = float(size.height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self.standalone_image.set_allocation(self.standalone_scroll.get_allocation())
self.standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self.standalone_image.show()
def _get_default_image(self):
return self._icon_theme.load_icon(
Utils.STOCK_ICON_DEFAULT,
512,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)

11
src/main.py Normal file
View file

@ -0,0 +1,11 @@
import sys
import gi
gi.require_version('Gtk', '3.0')
from .application import Application
def main(version):
app = Application()
return app.run(sys.argv)

25
src/mcg.in Executable file
View file

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

38
src/meson.build Normal file
View file

@ -0,0 +1,38 @@
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',
'infodialog.py',
'librarypanel.py',
'playlistpanel.py',
'serverpanel.py',
'shortcutsdialog.py',
'utils.py',
'window.py',
'zeroconf.py',
]
install_data(mcg_sources, install_dir: moduledir)

353
src/playlistpanel.py Normal file
View file

@ -0,0 +1,353 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import logging
import math
import threading
from gi.repository import Gtk, GObject, GdkPixbuf
from mcg import client
from mcg.albumheaderbar import AlbumHeaderbar
from mcg.utils import Utils
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/playlist-toolbar.ui')
class PlaylistToolbar(Gtk.ButtonBox):
__gtype_name__ = 'McgPlaylistToolbar'
__gsignals__ = {
'select': (GObject.SIGNAL_RUN_FIRST, None, (bool,)),
'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ())
}
# Widgets
playlist_clear_button = Gtk.Template.Child()
select_button = Gtk.Template.Child()
def __init__(self):
super().__init__()
@Gtk.Template.Callback()
def on_select_toggled(self, widget):
self.emit('select', widget.get_active())
@Gtk.Template.Callback()
def on_clear_clicked(self, widget):
if widget is self.playlist_clear_button:
self.emit('clear-playlist')
def exit_selection(self):
self.select_button.set_active(False)
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/playlist-panel.ui')
class PlaylistPanel(Gtk.Stack):
__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
panel_standalone = Gtk.Template.Child()
actionbar_revealer = 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):
GObject.GObject.__init__(self)
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_default()
self._standalone_pixbuf = None
self._selected_albums = []
self._is_selected = False
# Widgets
self._toolbar = PlaylistToolbar()
self._toolbar.connect('select', self.on_toolbar_select)
self._toolbar.connect('clear-playlist', self.on_toolbar_clear)
# Header bar
self._headerbar_standalone = AlbumHeaderbar()
self._headerbar_standalone.connect('close', self.on_headerbar_close_clicked)
# Playlist Grid: Model
self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
# Playlist Grid
self.playlist_grid.set_model(self._playlist_grid_model)
self.playlist_grid.set_pixbuf_column(0)
self.playlist_grid.set_text_column(-1)
self.playlist_grid.set_tooltip_column(1)
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
def on_toolbar_select(self, widget, active):
if active:
self.actionbar_revealer.set_reveal_child(True)
self.playlist_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
self.playlist_grid.get_style_context().add_class(Utils.CSS_SELECTION)
else:
self.actionbar_revealer.set_reveal_child(False)
self.playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.playlist_grid.get_style_context().remove_class(Utils.CSS_SELECTION)
def on_toolbar_clear(self, widget):
self.emit('clear-playlist')
@Gtk.Template.Callback()
def on_playlist_grid_clicked(self, widget, path):
# Get selected album
iter = self._playlist_grid_model.get_iter(path)
hash = self._playlist_grid_model.get_value(iter, 2)
album = self._playlist_albums[hash]
self._selected_albums = [album]
self.emit('albumart', hash)
# Show standalone album
if widget.get_selection_mode() == Gtk.SelectionMode.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_playlist_grid_selection_changed(self, widget):
self._selected_albums = []
for path in widget.get_selected_items():
iter = self._playlist_grid_model.get_iter(path)
hash = self._playlist_grid_model.get_value(iter, 2)
self._selected_albums.append(self._playlist_albums[hash])
@Gtk.Template.Callback()
def on_selection_cancel_clicked(self, widget):
self._toolbar.exit_selection()
@Gtk.Template.Callback()
def on_selection_remove_clicked(self, widget):
self.emit('remove-multiple-albums', self._selected_albums)
self._toolbar.exit_selection()
@Gtk.Template.Callback()
def on_standalone_scroll_size_allocate(self, widget, allocation):
self._resize_standalone_image()
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._selected_albums[0])
self._close_standalone()
@Gtk.Template.Callback()
def on_standalone_play_clicked(self, widget):
self.emit('play', self._selected_albums[0])
self._close_standalone()
def set_item_size(self, item_size):
if self._item_size != item_size:
self._item_size = item_size
self._redraw()
def get_item_size(self):
return self._item_size
def set_playlist(self, host, playlist):
self._host = host
self._playlist_stop.set()
threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
def set_albumart(self, album, data):
if album in self._selected_albums:
if data:
# Load image and draw it
try:
self._standalone_pixbuf = Utils.load_pixbuf(data)
except Exception as e:
self._logger.exception("Failed to set albumart")
self._cover_pixbuf = self._get_default_image()
else:
self._cover_pixbuf = self._get_default_image()
# Show image
GObject.idle_add(self._show_image)
def stop_threads(self):
self._playlist_stop.set()
def _set_playlist(self, host, playlist, size):
if not self._is_selected and self._playlist != playlist:
GObject.idle_add(
self.get_parent().child_set_property,
self,
'needs-attention',
True
)
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.set_model(None)
self.playlist_grid.freeze_child_notify()
self._playlist_grid_model.clear()
GObject.idle_add(self.playlist_grid.set_item_padding, size / 100)
cache = client.MCGCache(host, size)
for album in playlist:
pixbuf = None
# Load albumart thumbnail
try:
pixbuf = Utils.load_thumbnail(cache, self._client, album, size)
except client.CommandException:
# Exception is handled by client
pass
except Exception:
self._logger.exception("Failed to load albumart")
if pixbuf is None:
pixbuf = self._icon_theme.load_icon(
Utils.STOCK_ICON_DEFAULT,
self._item_size,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)
if pixbuf is not None:
self._playlist_grid_model.append([
pixbuf,
GObject.markup_escape_text("\n".join([
album.get_title(),
', '.join(album.get_dates()),
Utils.create_artists_label(album),
Utils.create_length_label(album)
])),
album.get_id()
])
if self._playlist_stop.is_set():
self._playlist_lock.release()
return
self.playlist_grid.set_model(self._playlist_grid_model)
self.playlist_grid.thaw_child_notify()
# TODO why set_columns()?
#self.playlist_grid.set_columns(len(playlist))
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.set_visible_child(self.panel_standalone)
self.emit('open-standalone')
def _close_standalone(self):
self.set_visible_child(self.get_children()[0])
self.emit('close-standalone')
def _resize_standalone_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
pixbuf = self._standalone_pixbuf
size = self.standalone_scroll.get_allocation()
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size.width) / float(pixbuf.get_width())
ratioH = float(size.height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self.standalone_image.set_allocation(self.standalone_scroll.get_allocation())
self.standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self.standalone_image.show()
def _get_default_image(self):
return self._icon_theme.load_icon(
Utils.STOCK_ICON_DEFAULT,
512,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)

129
src/serverpanel.py Normal file
View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/server-toolbar.ui')
class ServerToolbar(Gtk.ButtonBox):
__gtype_name__ = 'McgServerToolbar'
def __init__(self):
super().__init__()
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/server-panel.ui')
class ServerPanel(Gtk.Box):
__gtype_name__ = 'McgServerPanel'
__gsignals__ = {
'change-output-device': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,bool,)),
}
# Widgets
# Status widgets
status_file = Gtk.Template.Child()
status_audio = Gtk.Template.Child()
status_bitrate = Gtk.Template.Child()
status_error = Gtk.Template.Child()
# Stats widgets
stats_artists = Gtk.Template.Child()
stats_albums = Gtk.Template.Child()
stats_songs = Gtk.Template.Child()
stats_dbplaytime = Gtk.Template.Child()
stats_playtime = Gtk.Template.Child()
stats_uptime = Gtk.Template.Child()
# Audio ouptut devices widgets
output_devices = Gtk.Template.Child()
def __init__(self):
super().__init__()
self._none_label = ""
self._output_buttons = {}
self._is_selected = False
# Widgets
self._toolbar = ServerToolbar()
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(device.get_name())
if device.is_enabled():
button.set_active(True)
handler = button.connect('toggled', self.on_output_device_toggled, device)
self.output_devices.insert(button, -1)
self._output_buttons[device.get_id()] = button
self.output_devices.show_all()
# Remove devices
for id in self._output_buttons.keys():
if id not in device_ids:
self.output_devices.remove(self._output_buttons[id].get_parent())

18
src/shortcutsdialog.py Normal file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
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__()

View file

@ -14,45 +14,33 @@ 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
CSS_SELECTION = 'selection'
STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
def load_thumbnail(cache, album, size):
def load_pixbuf(data):
loader = GdkPixbuf.PixbufLoader()
try:
loader.write(data)
finally:
loader.close()
return loader.get_pixbuf()
def load_thumbnail(cache, client, album, size):
cache_url = cache.create_filename(album)
pixbuf = None
if os.path.isfile(cache_url):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url)
except Exception as e:
print(e)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_url)
else:
url = album.get_cover()
pixbuf = Utils.load_cover(url)
# Load cover from server
albumart = client.get_albumart_now(album.get_id())
if albumart:
pixbuf = Utils.load_pixbuf(albumart)
if pixbuf is not None:
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER)
filetype = os.path.splitext(url)[1][1:]
if filetype == 'jpg':
filetype = 'jpeg'
pixbuf.savev(cache.create_filename(album), filetype, [], [])
pixbuf.savev(cache_url, 'jpeg', [], [])
return pixbuf
@ -66,6 +54,13 @@ class Utils:
return label
def create_length_label(album):
minutes = album.get_length() // 60
seconds = album.get_length() - minutes * 60
return locale.gettext("{}:{} minutes").format(minutes, seconds)
def create_track_title(track):
title = track.get_title()
if track.get_artists():
@ -91,3 +86,4 @@ class SortOrder:
ARTIST = 0
TITLE = 1
YEAR = 2
MODIFIED = 3

660
src/window.py Normal file
View file

@ -0,0 +1,660 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
try:
import keyring
use_keyring = True
except:
use_keyring = False
import locale
import logging
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
from . import client
from .shortcutsdialog import ShortcutsDialog
from .connectionpanel import ConnectionPanel
from .serverpanel import ServerPanel
from .coverpanel import CoverPanel
from .playlistpanel import PlaylistPanel
from .librarypanel import LibraryPanel
from .zeroconf import ZeroconfProvider
class WindowState(GObject.Object):
WIDTH = 'width'
HEIGHT = 'height'
IS_MAXIMIZED = 'is_maximized'
IS_FULLSCREENED = 'is_fullscreened'
width = GObject.Property(type=int, default=800)
height = GObject.Property(type=int, default=600)
is_maximized = GObject.Property(type=bool, default=False)
is_fullscreened = GObject.Property(type=bool, default=False)
def __init__(self):
super().__init__()
@Gtk.Template(resource_path='/xyz/suruatoel/mcg/ui/window.ui')
class Window(Gtk.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'
_CUSTOM_STARTUP_COMPLETE = 'startup-complete'
# Widgets
content_stack = Gtk.Template.Child()
panel_stack = Gtk.Template.Child()
toolbar_stack = Gtk.Template.Child()
# Headerbar
headerbar = Gtk.Template.Child()
headerbar_title_stack = Gtk.Template.Child()
headerbar_panel_switcher = Gtk.Template.Child()
headerbar_connection_label = Gtk.Template.Child()
headerbar_button_connect = Gtk.Template.Child()
headerbar_button_playpause = Gtk.Template.Child()
headerbar_button_volume = Gtk.Template.Child()
# Infobar
info_revealer = Gtk.Template.Child()
info_bar = Gtk.Template.Child()
info_label = Gtk.Template.Child()
def __init__(self, app, title, settings):
super().__init__()
self.set_application(app)
self.set_title(title)
self._settings = settings
self._panels = []
self._mcg = client.Client()
self._state = WindowState()
self._changing_volume = False
self._setting_volume = False
# 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(self._connection_panel)
self.panel_stack.add_titled(self._server_panel, 'server-panel', locale.gettext("Server"))
self.panel_stack.add_titled(self._cover_panel, 'cover-panel', locale.gettext("Cover"))
self.panel_stack.add_titled(self._playlist_panel, 'playlist-panel', locale.gettext("Playlist"))
self.panel_stack.add_titled(self._library_panel, 'library-panel', locale.gettext("Library"))
# Header
self._playlist_panel.get_headerbar_standalone().connect('close', self.on_panel_close_standalone)
self._library_panel.get_headerbar_standalone().connect('close', self.on_panel_close_standalone)
# Toolbar stack
self.toolbar_stack.add(self._server_panel.get_toolbar())
self.toolbar_stack.add(self._cover_panel.get_toolbar())
self.toolbar_stack.add(self._playlist_panel.get_toolbar())
self.toolbar_stack.add(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._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_CUSTOM, self.on_mcg_custom)
self._mcg.connect_signal(client.Client.SIGNAL_ERROR, self.on_mcg_error)
self._settings.connect('changed::'+Window.SETTING_PANEL, self.on_settings_panel_changed)
self._settings.connect('changed::'+Window.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
self._settings.connect('changed::'+Window.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
self._settings.connect('changed::'+Window.SETTING_SORT_TYPE, self.on_settings_sort_type_changed)
self._settings.bind(Window.SETTING_WINDOW_WIDTH, self._state, WindowState.WIDTH, Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_HEIGHT, self._state, WindowState.HEIGHT, Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_MAXIMIZED, self._state, WindowState.IS_MAXIMIZED, Gio.SettingsBindFlags.DEFAULT)
# Actions
self.set_default_size(self._state.width, self._state.height)
if self._state.get_property(WindowState.IS_MAXIMIZED):
self.maximize()
self.show_all()
self.content_stack.set_visible_child(self._connection_panel)
if self._settings.get_boolean(Window.SETTING_CONNECTED):
self._connect()
# Menu actions
self._connect_action = Gio.SimpleAction.new_stateful("connect", None, GLib.Variant.new_boolean(False))
self._connect_action.connect('change-state', self.on_menu_connect)
self.add_action(self._connect_action)
self._play_action = Gio.SimpleAction.new_stateful("play", None, GLib.Variant.new_boolean(False))
self._play_action.set_enabled(False)
self._play_action.connect('change-state', self.on_menu_play)
self.add_action(self._play_action)
self._clear_playlist_action = Gio.SimpleAction.new("clear-playlist", None)
self._clear_playlist_action.set_enabled(False)
self._clear_playlist_action.connect('activate', self.on_menu_clear_playlist)
self.add_action(self._clear_playlist_action)
panel_variant = GLib.Variant.new_string("0")
self._panel_action = Gio.SimpleAction.new_stateful("panel", panel_variant.get_type(), panel_variant)
self._panel_action.set_enabled(False)
self._panel_action.connect('change-state', self.on_menu_panel)
self.add_action(self._panel_action)
self._toggle_fullscreen_action = Gio.SimpleAction.new("toggle-fullscreen", None)
self._toggle_fullscreen_action.set_enabled(True)
self._toggle_fullscreen_action.connect('activate', self.on_menu_toggle_fullscreen)
self.add_action(self._toggle_fullscreen_action)
self._search_library_action = Gio.SimpleAction.new("search-library", None)
self._search_library_action.set_enabled(True)
self._search_library_action.connect('activate', self.on_menu_search_library)
self.add_action(self._search_library_action)
# Menu callbacks
def on_menu_connect(self, action, value):
self._connect()
def on_menu_play(self, action, value):
self._mcg.playpause()
def on_menu_clear_playlist(self, action, value):
self._mcg.clear_playlist()
def on_menu_panel(self, action, value):
action.set_state(value)
self.panel_stack.set_visible_child(self._panels[int(value.get_string())])
def on_menu_toggle_fullscreen(self, action, value):
self.panel_stack.set_visible_child(self._cover_panel)
if not self._state.get_property(WindowState.IS_FULLSCREENED):
self.fullscreen()
else:
self.unfullscreen()
def on_menu_search_library(self, action, value):
self.panel_stack.set_visible_child(self._library_panel)
self._library_panel.show_search()
# Window callbacks
@Gtk.Template.Callback()
def on_resize(self, widget, event):
if not self._state.get_property(WindowState.IS_MAXIMIZED):
size = self.get_size()
self._state.set_property(WindowState.WIDTH, size.width)
self._state.set_property(WindowState.HEIGHT, size.height)
@Gtk.Template.Callback()
def on_state(self, widget, state):
self._state.set_property(WindowState.IS_MAXIMIZED, (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0))
self._fullscreen((state.new_window_state & Gdk.WindowState.FULLSCREEN > 0))
# HeaderBar callbacks
@Gtk.Template.Callback()
def on_headerbar_connection_active_notify(self, widget, status):
self._connect()
@Gtk.Template.Callback()
def on_headerbar_connection_state_set(self, widget, state):
return True
@Gtk.Template.Callback()
def on_headerbar_volume_press(self, widget, active):
self._changing_volume = active
@Gtk.Template.Callback()
def on_headerbar_volume_release(self, widget, active):
self._changing_volume = active
@Gtk.Template.Callback()
def on_headerbar_playpause_toggled(self, widget):
self._mcg.playpause()
self._mcg.get_status()
@Gtk.Template.Callback()
def on_headerbar_volume_changed(self, widget, value):
if not self._setting_volume:
self._mcg.set_volume(int(value*100))
# Infobar callback
@Gtk.Template.Callback()
def on_info_bar_close(self, *args):
self.info_revealer.set_reveal_child(False)
@Gtk.Template.Callback()
def on_info_bar_response(self, widget, response):
self.info_revealer.set_reveal_child(False)
# 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())
GObject.idle_add(
self.panel_stack.child_set_property,
self.panel_stack.get_visible_child(),
'needs-attention',
False
)
def on_panel_open_standalone(self, panel):
self.set_titlebar(panel.get_headerbar_standalone())
def on_panel_close_standalone(self, headerbar):
self.set_titlebar(self.headerbar)
def on_connection_panel_connection_changed(self, widget, host, port, password):
self._settings.set_string(Window.SETTING_HOST, host)
self._settings.set_int(Window.SETTING_PORT, port)
if use_keyring:
if password:
keyring.set_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME, password)
else:
if keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME):
keyring.delete_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME)
def on_playlist_panel_clear_playlist(self, widget):
self._mcg.clear_playlist()
def on_playlist_panel_remove(self, widget, album):
self._mcg.remove_album_from_playlist(album)
def on_playlist_panel_remove_multiple(self, widget, albums):
self._mcg.remove_albums_from_playlist(albums)
def on_playlist_panel_play(self, widget, album):
self._mcg.play_album_from_playlist(album)
def on_playlist_panel_albumart(self, widget, album):
self._mcg.get_albumart(album)
def on_server_panel_output_device_changed(self, widget, device, enabled):
self._mcg.enable_output_device(device, enabled)
def on_cover_panel_toggle_fullscreen(self, widget):
if not self._state.get_property(WindowState.IS_FULLSCREENED):
self.fullscreen()
else:
self.unfullscreen()
def on_cover_panel_set_song(self, widget, pos, time):
self._mcg.seek(pos, time)
def on_cover_panel_albumart(self, widget, album):
self._mcg.get_albumart(album)
def on_library_panel_update(self, widget):
self._mcg.update()
def on_library_panel_play(self, widget, album):
self._mcg.play_album(album)
def on_library_panel_queue(self, widget, album):
self._mcg.queue_album(album)
def on_library_panel_queue_multiple(self, widget, albums):
self._mcg.queue_albums(albums)
def on_library_panel_item_size_changed(self, widget, size):
self._playlist_panel.set_item_size(size)
self._settings.set_int(Window.SETTING_ITEM_SIZE, self._library_panel.get_item_size())
def on_library_panel_sort_order_changed(self, widget, sort_order):
self._settings.set_enum(Window.SETTING_SORT_ORDER, self._library_panel.get_sort_order())
def on_library_panel_sort_type_changed(self, widget, sort_type):
self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._library_panel.get_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_custom(Window._CUSTOM_STARTUP_COMPLETE)
self._mcg.get_status()
self._mcg.get_stats()
self._mcg.get_output_devices()
self._connect_action.set_state(GLib.Variant.new_boolean(True))
self._play_action.set_enabled(True)
self._clear_playlist_action.set_enabled(True)
self._panel_action.set_enabled(True)
else:
GObject.idle_add(self._connect_disconnected)
self._connect_action.set_state(GLib.Variant.new_boolean(False))
self._play_action.set_enabled(False)
self._clear_playlist_action.set_enabled(False)
self._panel_action.set_enabled(False)
def on_mcg_status(self, state, album, pos, time, volume, file, audio, bitrate, error):
# Album
GObject.idle_add(self._cover_panel.set_album, album)
if not album and self._state.get_property(WindowState.IS_FULLSCREENED):
self._fullscreen(False)
# State
if state == 'play':
GObject.idle_add(self._set_play)
GObject.idle_add(self._cover_panel.set_play, pos, time)
self._play_action.set_state(GLib.Variant.new_boolean(True))
elif state == 'pause' or state == 'stop':
GObject.idle_add(self._set_pause)
GObject.idle_add(self._cover_panel.set_pause)
self._play_action.set_state(GLib.Variant.new_boolean(False))
# Volume
GObject.idle_add(self._set_volume, volume)
# Status
self._server_panel.set_status(file, audio, bitrate, error)
# Error
if error is None:
self.info_revealer.set_reveal_child(False)
else:
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_custom(self, name):
if name == Window._CUSTOM_STARTUP_COMPLETE:
for panel in self._panels:
GObject.idle_add(
self.panel_stack.child_set_property,
panel,
'needs-attention',
False
)
def on_mcg_error(self, error):
GObject.idle_add(self._show_error, str(error))
# Settings callbacks
def on_settings_panel_changed(self, settings, key):
panel_index = settings.get_int(key)
self.panel_stack.set_visible_child(self._panels[panel_index])
def on_settings_item_size_changed(self, settings, key):
size = settings.get_int(key)
self._playlist_panel.set_item_size(size)
self._library_panel.set_item_size(size)
def on_settings_sort_order_changed(self, settings, key):
sort_order = settings.get_enum(key)
self._library_panel.set_sort_order(sort_order)
def on_settings_sort_type_changed(self, settings, key):
sort_type = settings.get_boolean(key)
self._library_panel.set_sort_type(sort_type)
# Private methods
def _connect(self):
self._connection_panel.set_sensitive(False)
self._set_headerbar_sensitive(False, True)
if self._mcg.is_connected():
self._mcg.disconnect()
self._settings.set_boolean(Window.SETTING_CONNECTED, False)
else:
host = self._connection_panel.get_host()
port = self._connection_panel.get_port()
password = self._connection_panel.get_password()
self._mcg.connect(host, port, password)
self._settings.set_boolean(Window.SETTING_CONNECTED, True)
def _connect_connected(self):
self._headerbar_connected()
self._set_headerbar_sensitive(True, False)
self.content_stack.set_visible_child(self.panel_stack)
self.panel_stack.set_visible_child(self._panels[self._settings.get_int(Window.SETTING_PANEL)])
def _connect_disconnected(self):
self._playlist_panel.stop_threads();
self._library_panel.stop_threads();
self._headerbar_disconnected()
self._set_headerbar_sensitive(False, False)
self._save_visible_panel()
self.content_stack.set_visible_child(self._connection_panel)
self._connection_panel.set_sensitive(True)
def _fullscreen(self, fullscreened_new):
if fullscreened_new != self._state.get_property(WindowState.IS_FULLSCREENED):
self._state.set_property(WindowState.IS_FULLSCREENED, fullscreened_new)
if self._state.get_property(WindowState.IS_FULLSCREENED):
self.headerbar.hide()
self._cover_panel.set_fullscreen(True)
# Hide cursor
self.get_window().set_cursor(
Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "none")
)
else:
self.headerbar.show()
self._cover_panel.set_fullscreen(False)
# Reset cursor
self.get_window().set_cursor(
Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "default")
)
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_button_playpause.handler_block_by_func(
self.on_headerbar_playpause_toggled
)
self.headerbar_button_playpause.set_active(True)
self.headerbar_button_playpause.handler_unblock_by_func(
self.on_headerbar_playpause_toggled
)
def _set_pause(self):
self.headerbar_button_playpause.handler_block_by_func(
self.on_headerbar_playpause_toggled
)
self.headerbar_button_playpause.set_active(False)
self.headerbar_button_playpause.handler_unblock_by_func(
self.on_headerbar_playpause_toggled
)
def _set_volume(self, volume):
if volume >= 0:
self.headerbar_button_volume.set_visible(True)
if not self._changing_volume:
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_button_connect.handler_block_by_func(
self.on_headerbar_connection_active_notify
)
self.headerbar_button_connect.set_active(True)
self.headerbar_button_connect.set_state(True)
self.headerbar_button_connect.handler_unblock_by_func(
self.on_headerbar_connection_active_notify
)
self.headerbar_title_stack.set_visible_child(self.headerbar_panel_switcher)
def _headerbar_disconnected(self):
self.headerbar_button_connect.handler_block_by_func(
self.on_headerbar_connection_active_notify
)
self.headerbar_button_connect.set_active(False)
self.headerbar_button_connect.set_state(False)
self.headerbar_button_connect.handler_unblock_by_func(
self.on_headerbar_connection_active_notify
)
self.headerbar_title_stack.set_visible_child(self.headerbar_connection_label)
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_bar.set_message_type(Gtk.MessageType.ERROR)
self.info_label.set_text(message)
self.info_revealer.set_reveal_child(True)