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.
This commit is contained in:
coderkun 2020-08-02 17:18:28 +02:00
parent f4b545369c
commit ba373ddf4e
31 changed files with 4436 additions and 4305 deletions

View file

@ -1,11 +1,22 @@
<?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>
<file>icons/mcg.svg</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>

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

Before After
Before After

BIN
data/mcg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<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>

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

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<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>
<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>
</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>

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

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<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>
<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>
<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.36.0 -->
<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

@ -51,7 +51,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>

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

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<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">2.1.2</property>
<property name="comments" translatable="yes">CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.</property>
<property name="website">http://www.suruatoel.xyz/codes/mcg</property>
<property name="logo_icon_name">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>
<child type="titlebar">
<placeholder/>
</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.36.0 -->
<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>

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

@ -0,0 +1,232 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<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="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">5</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.36.0 -->
<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.36.0 -->
<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>

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

@ -0,0 +1,512 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<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>
<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>
</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>
<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>
</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.36.0 -->
<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,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
@ -118,5 +118,5 @@
</child>
</object>
</child>
</object>
</template>
</interface>

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

@ -0,0 +1,233 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="McgAppWindow" parent="GtkApplicationWindow">
<property name="can_focus">False</property>
<property name="icon_name">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>
<property name="icons">audio-volume-muted
audio-volume-high
audio-volume-low
audio-volume-medium</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>

35
mcg/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='/de/coderkun/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()))

View file

@ -1,16 +1,15 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import gettext
import logging
import urllib
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk, Gdk, GLib
from mcg import Environment
from mcg import widgets
from mcg.window import Window
from mcg.infodialog import InfoDialog
@ -32,7 +31,6 @@ class Application(Gtk.Application):
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([
@ -53,12 +51,9 @@ class Application(Gtk.Application):
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()
@ -66,22 +61,15 @@ class Application(Gtk.Application):
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 = Window(self, 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 = InfoDialog()
self._info_dialog.run()
self._info_dialog.hide()
def on_menu_quit(self, action, value):
@ -95,13 +83,6 @@ class Application(Gtk.Application):
)
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)
@ -121,23 +102,7 @@ class Application(Gtk.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)
@ -149,7 +114,7 @@ class Application(Gtk.Application):
def _load_appmenu(self):
builder = Gtk.Builder()
builder.set_translation_domain(Application.DOMAIN)
builder.add_from_resource(self._get_resource_path('gtk.menu.ui'))
builder.add_from_resource(self._get_resource_path('ui/gtk.menu.ui'))
self.set_app_menu(builder.get_object('app-menu'))

108
mcg/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='/de/coderkun/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(),)

279
mcg/coverpanel.py Normal file
View file

@ -0,0 +1,279 @@
#!/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='/de/coderkun/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='/de/coderkun/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
# Show image
self._resize_image()
self.cover_stack.set_visible_child(self.cover_scroll)
self.cover_spinner.stop()
self._current_cover_album = album
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 _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
)

21
mcg/infodialog.py Normal file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import logging
from gi.repository import Gtk, GObject, GdkPixbuf
@Gtk.Template(resource_path='/de/coderkun/mcg/ui/info-dialog.ui')
class InfoDialog(Gtk.AboutDialog):
__gtype_name__ = 'McgInfoDialog'
def __init__(self):
super().__init__()
self._logger = logging.getLogger(__name__)

635
mcg/librarypanel.py Normal file
View file

@ -0,0 +1,635 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
import gettext
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='/de/coderkun/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()
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
}
@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)
@Gtk.Template(resource_path='/de/coderkun/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 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:
button = self._toolbar_sort_buttons[sort]
if button and not button.get_active():
button.set_active(True)
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:
if sort_type:
sort_type_gtk = Gtk.SortType.DESCENDING
self.toolbar_sort_order_button.set_active(True)
else:
sort_type_gtk = Gtk.SortType.ASCENDING
self.toolbar_sort_order_button.set_active(False)
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(gettext.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
self._resize_standalone_image()
self.standalone_stack.set_visible_child(self.standalone_scroll)
self.standalone_spinner.stop()
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, gettext.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 _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
)

View file

@ -3,17 +3,42 @@
import sys
from mcg.application import Application
from mcg import Environment
def setup_resources():
from gi.repository import Gio
resource = Gio.resource_load(
Environment.get_data('de.coderkun.mcg.gresource')
)
Gio.Resource._register(resource)
def setup_locale():
import gettext
gettext.bindtextdomain('mcg', Environment.get_locale())
gettext.textdomain('mcg')
def run_application():
from mcg.application import Application
app = Application()
return app.run(sys.argv)
def main():
# Start application
app = Application()
exit_status = app.run(sys.argv)
sys.exit(exit_status)
setup_resources()
setup_locale()
return run_application()
if __name__ == "__main__":
main()
sys.exit(main())

349
mcg/playlistpanel.py Normal file
View file

@ -0,0 +1,349 @@
#!/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='/de/coderkun/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='/de/coderkun/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
self._resize_standalone_image()
self.standalone_stack.set_visible_child(self.standalone_scroll)
self.standalone_spinner.stop()
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 _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
mcg/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='/de/coderkun/mcg/ui/server-toolbar.ui')
class ServerToolbar(Gtk.ButtonBox):
__gtype_name__ = 'McgServerToolbar'
def __init__(self):
super().__init__()
@Gtk.Template(resource_path='/de/coderkun/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
mcg/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='/de/coderkun/mcg/ui/shortcuts-dialog.ui')
class ShortcutsDialog(Gtk.ShortcutsWindow):
__gtype_name__ = 'McgShortcutsDialog'
def __init__(self):
super().__init__()

View file

@ -14,6 +14,8 @@ from gi.repository import GdkPixbuf
class Utils:
CSS_SELECTION = 'selection'
STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
def load_pixbuf(data):

File diff suppressed because it is too large Load diff

650
mcg/window.py Normal file
View file

@ -0,0 +1,650 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
try:
import keyring
use_keyring = True
except:
use_keyring = False
import logging
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
from mcg import client
from mcg.shortcutsdialog import ShortcutsDialog
from mcg.connectionpanel import ConnectionPanel
from mcg.serverpanel import ServerPanel
from mcg.coverpanel import CoverPanel
from mcg.playlistpanel import PlaylistPanel
from mcg.librarypanel import LibraryPanel
from mcg.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='/de/coderkun/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', "Server")
self.panel_stack.add_titled(self._cover_panel, 'cover-panel', "Cover")
self.panel_stack.add_titled(self._playlist_panel, 'playlist-panel', "Playlist")
self.panel_stack.add_titled(self._library_panel, 'library-panel', "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)
# 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()
# 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):
GObject.idle_add(self._cover_panel.set_albumart, album, data)
GObject.idle_add(self._playlist_panel.set_albumart, album, data)
GObject.idle_add(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)