diff --git a/data/de.coderkun.mcg.gresource.xml b/data/de.coderkun.mcg.gresource.xml
index 02969fc..bd04296 100644
--- a/data/de.coderkun.mcg.gresource.xml
+++ b/data/de.coderkun.mcg.gresource.xml
@@ -1,11 +1,22 @@
- gtk.glade
gtk.css
- gtk.shortcuts.ui
- gtk.menu.ui
- mcg.svg
noise-texture.png
+ icons/mcg.svg
+ ui/window.ui
+ ui/gtk.menu.ui
+ ui/info-dialog.ui
+ ui/shortcuts-dialog.ui
+ ui/connection-panel.ui
+ ui/album-headerbar.ui
+ ui/server-toolbar.ui
+ ui/server-panel.ui
+ ui/cover-toolbar.ui
+ ui/cover-panel.ui
+ ui/playlist-toolbar.ui
+ ui/playlist-panel.ui
+ ui/library-toolbar.ui
+ ui/library-panel.ui
diff --git a/data/gtk.glade b/data/gtk.glade
deleted file mode 100644
index fd77059..0000000
--- a/data/gtk.glade
+++ /dev/null
@@ -1,1975 +0,0 @@
-
-
-
-
-
-
- True
- False
-
-
- True
- True
- True
-
-
-
- True
- False
- go-previous-symbolic
-
-
-
-
-
-
- True
- False
- vertical
-
-
- True
- False
- Title
- True
-
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- Artist
- True
-
-
- False
- True
- 1
-
-
-
-
-
-
- 100
- 1000
- 150
- 1
- 10
-
-
- False
-
-
- True
- False
- vertical
-
-
- 350
- True
- True
- library-scale-adjustment
- False
- -1
- 0
- 0
- False
-
-
-
-
- True
- True
- 0
-
-
-
-
- gtk-refresh
- True
- True
- True
- none
- True
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- vertical
-
-
- True
- False
- vertical
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- Sort
-
-
- False
- True
- 1
-
-
-
-
- sort by artist
- True
- True
- False
- True
- library-toolbar-sort-year
-
-
-
- False
- True
- 2
-
-
-
-
- sort by title
- True
- True
- False
- True
- library-toolbar-sort-year
-
-
-
- False
- True
- 3
-
-
-
-
- sort by year
- True
- True
- False
- True
- True
-
-
-
- False
- True
- 4
-
-
-
-
- gtk-sort-descending
- True
- True
- False
- True
- True
- True
-
-
-
- False
- True
- 5
-
-
-
-
- False
- True
- 2
-
-
-
-
-
-
- 1024
- 9999
- 6600
- 1
- 100
-
-
- False
- mcg.svg
-
-
-
-
-
-
-
- True
- False
-
-
- True
- False
- 100
- crossfade
-
-
- True
- False
- vertical
-
-
- True
- False
-
-
- 500
- True
- False
- 5
- True
- True
-
-
- True
- False
-
-
- True
- True
-
-
-
-
-
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
- 5
- vertical
-
-
- False
- True
- end
- 1
-
-
-
-
- 0
- 0
- 6
-
-
-
-
- True
- True
- localhost
- Enter hostname or IP address
-
-
-
- 1
- 1
-
-
-
-
- True
- True
- False
- Enter password or leave blank
- password
-
-
-
- 1
- 5
-
-
-
-
- True
- True
- 6600
- server-port-adjustment
- 6600
-
-
-
- 1
- 3
-
-
-
-
- True
- False
- start
- Host:
-
-
- 1
- 0
-
-
-
-
- True
- False
- start
- Port:
-
-
- 1
- 2
-
-
-
-
- True
- False
- start
- Password:
-
-
- 1
- 4
-
-
-
-
- True
- False
- 0
-
-
-
-
- True
- False
- 0
-
-
-
-
- page1
- page1
-
-
-
-
- True
- False
-
-
- True
- False
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
-
-
- True
- False
-
-
-
-
-
- True
- False
- vertical
-
-
-
-
-
- True
- False
- 5
- 5
- 5
- 5
- vertical
- 10
-
-
- True
- False
- dialog-information-symbolic
- 6
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 2
- 5
-
-
- True
- False
- start
- start
- File:
-
-
- 0
- 0
-
-
-
-
- True
- False
- start
- start
- Audio:
-
-
- 0
- 1
-
-
-
-
- True
- False
- start
- start
- Bitrate:
-
-
- 0
- 2
-
-
-
-
- True
- False
- start
- start
- Error:
-
-
- 0
- 3
-
-
-
-
- True
- False
- start
- <i>none</i>
- True
- True
- True
- 0
-
-
- 1
- 2
-
-
-
-
- True
- False
- start
- <i>none</i>
- True
- True
- True
- 0
-
-
- 1
- 1
-
-
-
-
- True
- False
- start
- <i>none</i>
- True
- True
- True
- 0
-
-
- 1
- 0
-
-
-
-
- True
- False
- start
- <i>none</i>
- True
- True
- True
- 0
-
-
- 1
- 3
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- page0
- Status
-
-
-
-
- True
- False
-
-
-
-
-
- True
- False
- vertical
-
-
-
-
-
- True
- False
- vertical
- 10
-
-
- True
- False
- starred-symbolic
- 6
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 2
- 5
-
-
- True
- False
- end
- right
- True
-
-
- 0
- 0
-
-
-
-
- True
- False
- end
- right
-
-
- 0
- 1
-
-
-
-
- True
- False
- end
- right
-
-
- 0
- 2
-
-
-
-
- True
- False
- start
- Albums
-
-
- 1
- 1
-
-
-
-
- True
- False
- start
- Songs
-
-
- 1
- 2
-
-
-
-
- True
- False
- start
- Artists
-
-
- 1
- 0
-
-
-
-
- True
- False
- end
- right
-
-
- 0
- 3
-
-
-
-
- True
- False
- start
- Seconds
-
-
- 1
- 3
-
-
-
-
- True
- False
- end
- right
-
-
- 0
- 6
-
-
-
-
- True
- False
- end
- right
-
-
- 0
- 5
-
-
-
-
- True
- False
-
-
- 0
- 4
- 2
-
-
-
-
- True
- False
- start
- Seconds played
-
-
- 1
- 5
-
-
-
-
- True
- False
- Seconds running
-
-
- 1
- 6
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- page1
- Statistics
- 1
-
-
-
-
- True
- False
-
-
-
-
-
- True
- False
- vertical
-
-
-
-
-
- True
- False
- vertical
- 10
-
-
- True
- False
- audio-speakers-symbolic
- 6
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- none
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- page2
- Audio Devices
- 2
-
-
-
-
- True
- True
- 1
-
-
-
-
- server
- Server
-
-
-
-
- True
- False
-
-
- True
- False
-
-
- True
- False
-
-
- cover-spinner
-
-
-
-
- True
- True
- False
- False
-
-
-
- True
- False
-
-
- True
- False
-
-
-
- True
- False
- 0
-
-
-
-
-
-
-
-
- cover-scroll
- 1
-
-
-
-
- -1
-
-
-
-
- True
- False
- end
- slide-right
-
-
- True
- True
- start
- never
- 200
- True
-
-
- True
- False
- none
-
-
- True
- False
- start
- 5
- 5
- 5
- 5
- vertical
-
-
- True
- False
- 5
- 5
- 5
- True
-
-
- True
- False
- start
- Album
- True
- 0
-
-
- 0
- 0
-
-
-
-
- True
- False
- start
- Date
- True
- 0
-
-
- 0
- 1
-
-
-
-
- True
- False
- start
- Artist
- True
- 0
-
-
- 0
- 2
-
-
-
-
-
- False
- True
- 10
- 0
-
-
-
-
- True
- True
- vertical
- False
- 0
- False
-
-
-
-
- True
- True
- end
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
- cover
- Cover
- 1
-
-
-
-
- True
- False
- slide-left-right
-
-
- True
- False
- vertical
-
-
- True
- True
-
-
- True
- True
- 0
- horizontal
- 0
- 0
- 1
- 0
- True
-
-
-
-
-
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
- slide-up
-
-
- True
- False
-
-
-
-
- False
- True
- 1
-
-
-
-
- page2
- page2
-
-
-
-
- True
- False
- vertical
-
-
- True
- False
-
-
- True
- False
-
-
- standalone-spinne
-
-
-
-
- True
- True
- False
- False
-
-
- True
- False
-
-
- True
- False
- gtk-missing-image
- 6
-
-
-
-
-
-
- standalone-scroll
- 1
-
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
-
-
- False
- True
- 1
-
-
-
-
- page1
- page1
- 1
-
-
-
-
- playlist
- Playlist
- 2
-
-
-
-
- True
- False
- slide-left-right
-
-
- True
- False
- vertical
-
-
- True
- True
- False
-
-
-
- True
- True
- edit-find-symbolic
- False
- False
- search library
-
-
-
-
-
- False
- True
- 0
-
-
-
-
-
-
-
- True
- False
- crossfade
-
-
- True
- False
- vertical
-
-
-
-
-
- True
- False
- vertical
-
-
-
-
-
- True
- False
- center
- vertical
- 10
-
-
- True
- False
- 6
-
-
- False
- True
- 0
-
-
-
-
- 200
- True
- False
- 0.01
- True
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 1
-
-
-
-
- page1
- page1
-
-
-
-
- True
- True
-
-
- True
- True
- 0
- horizontal
- 0
- 0
- 1
- 0
- True
-
-
-
-
-
-
-
-
-
- page0
- page0
- 1
-
-
-
-
- True
- True
- 2
-
-
-
-
- True
- False
- none
-
-
- True
- False
-
-
-
-
- False
- True
- 3
-
-
-
-
- page0
- page0
-
-
-
-
- True
- False
- vertical
-
-
- True
- False
- crossfade
-
-
- True
- False
-
-
- standalone-spinne
-
-
-
-
- True
- True
- False
- False
-
-
-
- True
- False
-
-
- True
- False
- gtk-missing-image
- 6
-
-
-
-
-
-
- standalone-scroll
- 1
-
-
-
-
- True
- True
- 0
-
-
-
-
- True
- False
-
-
- False
- True
- 1
-
-
-
-
- page1
- page1
- 1
-
-
-
-
- library
- Library
- 3
-
-
-
-
- page0
- page0
- 1
-
-
-
-
- -1
-
-
-
-
- True
- False
- start
-
-
- False
- start
- 10
- 10
- vertical
- top
- True
-
-
-
-
- False
-
-
- False
- False
- 0
-
-
-
-
- False
-
-
- True
- False
-
-
- False
- True
- 0
-
-
-
-
- False
- False
- 0
-
-
-
-
-
-
-
-
-
-
-
- False
- False
- True
- center
- dialog
- center
- appwindow
- CoverGrid
- 2.1.2
- CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.
- http://www.suruatoel.xyz/codes/mcg
- mcg.svg
- gpl-3-0
-
-
-
-
-
- False
- vertical
- 2
-
-
- False
- end
-
-
-
-
-
-
-
-
- False
- False
- 0
-
-
-
-
-
-
-
-
-
diff --git a/data/mcg.svg b/data/icons/mcg.svg
similarity index 68%
rename from data/mcg.svg
rename to data/icons/mcg.svg
index ae0a7c5..1cebef7 100644
--- a/data/mcg.svg
+++ b/data/icons/mcg.svg
@@ -1,6 +1,4 @@
-
-
diff --git a/data/mcg.png b/data/mcg.png
new file mode 100644
index 0000000..e992960
Binary files /dev/null and b/data/mcg.png differ
diff --git a/data/ui/album-headerbar.ui b/data/ui/album-headerbar.ui
new file mode 100644
index 0000000..a384338
--- /dev/null
+++ b/data/ui/album-headerbar.ui
@@ -0,0 +1,61 @@
+
+
+
+
+
+
diff --git a/data/ui/connection-panel.ui b/data/ui/connection-panel.ui
new file mode 100644
index 0000000..7f8000e
--- /dev/null
+++ b/data/ui/connection-panel.ui
@@ -0,0 +1,163 @@
+
+
+
+
+
+ 1024
+ 9999
+ 6600
+ 1
+ 100
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+
+
+ 500
+ True
+ False
+ 5
+ True
+ True
+
+
+ True
+ False
+
+
+ True
+ True
+
+
+
+
+
+
+
+
+ True
+ True
+ 0
+
+
+
+
+ True
+ False
+ 5
+ vertical
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ 0
+ 0
+ 6
+
+
+
+
+ True
+ True
+ localhost
+ Enter hostname or IP address
+
+
+
+ 1
+ 1
+
+
+
+
+ True
+ True
+ False
+ Enter password or leave blank
+ password
+
+
+
+ 1
+ 5
+
+
+
+
+ True
+ True
+ 6600
+ number
+ server-port-adjustment
+ 6600
+
+
+
+ 1
+ 3
+
+
+
+
+ True
+ False
+ start
+ Host:
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ start
+ Port:
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ start
+ Password:
+
+
+ 1
+ 4
+
+
+
+
+ True
+ False
+ 0
+
+
+
+
+ True
+ False
+ 0
+
+
+
+
diff --git a/data/ui/cover-panel.ui b/data/ui/cover-panel.ui
new file mode 100644
index 0000000..aca3b61
--- /dev/null
+++ b/data/ui/cover-panel.ui
@@ -0,0 +1,180 @@
+
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+ cover-spinner
+
+
+
+
+ True
+ True
+ False
+ False
+
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+
+ True
+ False
+ 0
+
+
+
+
+
+
+
+
+ cover-scroll
+ 1
+
+
+
+
+ -1
+
+
+
+
+ True
+ False
+ end
+ slide-right
+
+
+ True
+ True
+ start
+ never
+ 200
+ True
+
+
+ True
+ False
+ none
+
+
+ True
+ False
+ start
+ 5
+ 5
+ 5
+ 5
+ vertical
+
+
+ True
+ False
+ 5
+ 5
+ 5
+ True
+
+
+ True
+ False
+ start
+ Album
+ True
+ 0
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ start
+ Date
+ True
+ 0
+
+
+ 0
+ 1
+
+
+
+
+ True
+ False
+ start
+ Artist
+ True
+ 0
+
+
+ 0
+ 2
+
+
+
+
+
+ False
+ True
+ 10
+ 0
+
+
+
+
+ True
+ True
+ vertical
+ False
+ 0
+ False
+
+
+
+
+ True
+ True
+ end
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/ui/cover-toolbar.ui b/data/ui/cover-toolbar.ui
new file mode 100644
index 0000000..338cee3
--- /dev/null
+++ b/data/ui/cover-toolbar.ui
@@ -0,0 +1,33 @@
+
+
+
+
+
+ True
+ False
+ 6
+ end
+
+
+ True
+ True
+ True
+ Show the cover in fullscreen mode
+ win.toggle-fullscreen
+
+
+ True
+ False
+ view-fullscreen-symbolic
+
+
+
+
+ False
+ False
+ 0
+ True
+
+
+
+
diff --git a/data/gtk.menu.ui b/data/ui/gtk.menu.ui
similarity index 97%
rename from data/gtk.menu.ui
rename to data/ui/gtk.menu.ui
index e12fb59..a71ccb6 100644
--- a/data/gtk.menu.ui
+++ b/data/ui/gtk.menu.ui
@@ -51,7 +51,7 @@
-
- app.shortcuts
+ win.show-help-overlay
Keyboard Shortcuts
<Primary>k
diff --git a/data/ui/info-dialog.ui b/data/ui/info-dialog.ui
new file mode 100644
index 0000000..be9a804
--- /dev/null
+++ b/data/ui/info-dialog.ui
@@ -0,0 +1,49 @@
+
+
+
+
+
+ False
+ False
+ True
+ center
+ dialog
+ center
+ CoverGrid
+ 2.1.2
+ CoverGrid is a client for the Music Player Daemon, focusing on albums instead of single tracks.
+ http://www.suruatoel.xyz/codes/mcg
+ mcg
+ gpl-3-0
+
+
+ False
+ vertical
+ 2
+
+
+ False
+ end
+
+
+
+
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/ui/library-panel.ui b/data/ui/library-panel.ui
new file mode 100644
index 0000000..de06df0
--- /dev/null
+++ b/data/ui/library-panel.ui
@@ -0,0 +1,303 @@
+
+
+
+
+
+ True
+ False
+ slide-left-right
+
+
+ True
+ False
+ vertical
+
+
+ True
+ True
+ False
+
+
+
+ True
+ True
+ edit-find-symbolic
+ False
+ False
+ search library
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+
+
+
+ True
+ False
+ crossfade
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ center
+ vertical
+ 10
+
+
+ True
+ False
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ 200
+ True
+ False
+ 0
+ True
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ page1
+ page1
+
+
+
+
+ True
+ True
+
+
+ True
+ True
+ 0
+ horizontal
+ 0
+ 0
+ 1
+ 0
+ True
+
+
+
+
+
+
+
+
+
+ page0
+ page0
+ 1
+
+
+
+
+ True
+ True
+ 2
+
+
+
+
+ True
+ False
+ none
+
+
+ True
+ False
+
+
+ cancel
+ True
+ True
+ True
+
+
+
+ end
+ 1
+
+
+
+
+ queue
+ True
+ True
+ True
+
+
+
+ end
+ 0
+
+
+
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ page0
+ page0
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ crossfade
+
+
+ True
+ False
+
+
+ standalone-spinne
+
+
+
+
+ True
+ True
+ False
+ False
+
+
+
+ True
+ False
+
+
+ True
+ False
+ gtk-missing-image
+ 6
+
+
+
+
+
+
+ standalone-scroll
+ 1
+
+
+
+
+ True
+ True
+ 0
+
+
+
+
+ True
+ False
+
+
+ queue
+ True
+ True
+ True
+
+
+
+ end
+ 1
+
+
+
+
+ play
+ True
+ True
+ True
+
+
+
+ end
+ 0
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ page1
+ page1
+ 1
+
+
+
+
diff --git a/data/ui/library-toolbar.ui b/data/ui/library-toolbar.ui
new file mode 100644
index 0000000..dc63149
--- /dev/null
+++ b/data/ui/library-toolbar.ui
@@ -0,0 +1,232 @@
+
+
+
+
+
+ 100
+ 1000
+ 150
+ 1
+ 10
+
+
+ False
+
+
+ True
+ False
+ vertical
+
+
+ 350
+ True
+ True
+ grid_adjustment
+ False
+ -1
+ 0
+ 0
+ False
+
+
+
+
+ True
+ True
+ 0
+
+
+
+
+ gtk-refresh
+ True
+ True
+ True
+ none
+ True
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ Sort
+
+
+ False
+ True
+ 1
+
+
+
+
+ sort by artist
+ True
+ True
+ False
+ True
+ sort_year
+
+
+
+ False
+ True
+ 2
+
+
+
+
+ sort by title
+ True
+ True
+ False
+ True
+ sort_year
+
+
+
+ False
+ True
+ 3
+
+
+
+
+ sort by year
+ True
+ True
+ False
+ True
+ True
+
+
+
+ False
+ True
+ 4
+
+
+
+
+ gtk-sort-descending
+ True
+ True
+ False
+ True
+ True
+ True
+
+
+
+ False
+ True
+ 5
+
+
+
+
+ False
+ True
+ 2
+
+
+
+
+
+
+ True
+ False
+ 6
+ end
+
+
+ True
+ True
+ True
+ Search the library
+
+
+
+ True
+ False
+ system-search-symbolic
+
+
+
+
+
+ False
+ False
+ 0
+ True
+
+
+
+
+ True
+ True
+ True
+ Select multiple albums
+
+
+
+ True
+ False
+ object-select-symbolic
+
+
+
+
+ False
+ False
+ 1
+ True
+
+
+
+
+
+ False
+ False
+ 2
+ True
+
+
+
+
diff --git a/data/ui/playlist-panel.ui b/data/ui/playlist-panel.ui
new file mode 100644
index 0000000..a2a31ce
--- /dev/null
+++ b/data/ui/playlist-panel.ui
@@ -0,0 +1,194 @@
+
+
+
+
+
+ True
+ False
+ slide-left-right
+
+
+ True
+ False
+ vertical
+
+
+ True
+ True
+
+
+ True
+ True
+ 0
+ horizontal
+ 0
+ 0
+ 1
+ 0
+ True
+
+
+
+
+
+
+
+
+ True
+ True
+ 0
+
+
+
+
+ True
+ False
+ slide-up
+
+
+ True
+ False
+
+
+ cancel
+ True
+ True
+ True
+
+
+
+ end
+ 1
+
+
+
+
+ remove
+ True
+ True
+ True
+
+
+
+ end
+ 0
+
+
+
+
+
+
+ False
+ True
+ end
+ 1
+
+
+
+
+ page2
+ page2
+
+
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+ standalone-spinne
+
+
+
+
+ True
+ True
+ False
+ False
+
+
+
+ True
+ False
+
+
+ True
+ False
+ gtk-missing-image
+ 6
+
+
+
+
+
+
+ standalone-scroll
+ 1
+
+
+
+
+ True
+ True
+ 0
+
+
+
+
+ True
+ False
+
+
+ remove
+ True
+ True
+ True
+
+
+
+ end
+ 1
+
+
+
+
+ play
+ True
+ True
+ True
+
+
+
+ end
+ 0
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ page1
+ page1
+ 1
+
+
+
+
diff --git a/data/ui/playlist-toolbar.ui b/data/ui/playlist-toolbar.ui
new file mode 100644
index 0000000..c061395
--- /dev/null
+++ b/data/ui/playlist-toolbar.ui
@@ -0,0 +1,55 @@
+
+
+
+
+
+ True
+ False
+ 6
+ end
+
+
+ True
+ True
+ True
+ Select multiple albums
+
+
+
+ True
+ False
+ object-select-symbolic
+
+
+
+
+ False
+ False
+ 0
+ True
+
+
+
+
+ True
+ True
+ True
+ Clear the playlist
+
+
+
+ True
+ False
+ edit-clear
+
+
+
+
+ False
+ False
+ 1
+ True
+
+
+
+
diff --git a/data/ui/server-panel.ui b/data/ui/server-panel.ui
new file mode 100644
index 0000000..568648d
--- /dev/null
+++ b/data/ui/server-panel.ui
@@ -0,0 +1,512 @@
+
+
+
+
+
+ True
+ False
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ 5
+ 5
+ 5
+ 5
+ vertical
+ 10
+
+
+ True
+ False
+ dialog-information-symbolic
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ 2
+ 5
+
+
+ True
+ False
+ start
+ start
+ File:
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ start
+ start
+ Audio:
+
+
+ 0
+ 1
+
+
+
+
+ True
+ False
+ start
+ start
+ Bitrate:
+
+
+ 0
+ 2
+
+
+
+
+ True
+ False
+ start
+ start
+ Error:
+
+
+ 0
+ 3
+
+
+
+
+ True
+ False
+ start
+ <i>none</i>
+ True
+ True
+ True
+ 0
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ start
+ <i>none</i>
+ True
+ True
+ True
+ 0
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ start
+ <i>none</i>
+ True
+ True
+ True
+ 0
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ start
+ <i>none</i>
+ True
+ True
+ True
+ 0
+
+
+ 1
+ 3
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ page0
+ Status
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+ 10
+
+
+ True
+ False
+ starred-symbolic
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ 2
+ 5
+
+
+ True
+ False
+ end
+ right
+ True
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ end
+ right
+
+
+ 0
+ 1
+
+
+
+
+ True
+ False
+ end
+ right
+
+
+ 0
+ 2
+
+
+
+
+ True
+ False
+ start
+ Albums
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ start
+ Songs
+
+
+ 1
+ 2
+
+
+
+
+ True
+ False
+ start
+ Artists
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ end
+ right
+
+
+ 0
+ 3
+
+
+
+
+ True
+ False
+ start
+ Seconds
+
+
+ 1
+ 3
+
+
+
+
+ True
+ False
+ end
+ right
+
+
+ 0
+ 6
+
+
+
+
+ True
+ False
+ end
+ right
+
+
+ 0
+ 5
+
+
+
+
+ True
+ False
+
+
+ 0
+ 4
+ 2
+
+
+
+
+ True
+ False
+ start
+ Seconds played
+
+
+ 1
+ 5
+
+
+
+
+ True
+ False
+ Seconds running
+
+
+ 1
+ 6
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ page1
+ Statistics
+ 1
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ vertical
+
+
+ True
+ False
+ vertical
+ 10
+
+
+ True
+ False
+ audio-speakers-symbolic
+ 6
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ none
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+
+
+ page2
+ Audio Devices
+ 2
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
diff --git a/data/ui/server-toolbar.ui b/data/ui/server-toolbar.ui
new file mode 100644
index 0000000..c7561c1
--- /dev/null
+++ b/data/ui/server-toolbar.ui
@@ -0,0 +1,14 @@
+
+
+
+
+
+ True
+ False
+ vertical
+ start
+
+
+
+
+
diff --git a/data/gtk.shortcuts.ui b/data/ui/shortcuts-dialog.ui
similarity index 98%
rename from data/gtk.shortcuts.ui
rename to data/ui/shortcuts-dialog.ui
index 048cc3b..97dbc66 100644
--- a/data/gtk.shortcuts.ui
+++ b/data/ui/shortcuts-dialog.ui
@@ -1,7 +1,7 @@
-
+
True
@@ -118,5 +118,5 @@
-
+
diff --git a/data/ui/window.ui b/data/ui/window.ui
new file mode 100644
index 0000000..0acc759
--- /dev/null
+++ b/data/ui/window.ui
@@ -0,0 +1,233 @@
+
+
+
+
+
+ False
+ mcg
+
+
+
+
+ True
+ False
+
+
+ True
+ False
+ 100
+ crossfade
+
+
+ True
+ False
+
+
+
+
+
+ page0
+ page0
+
+
+
+
+ -1
+
+
+
+
+ True
+ False
+ start
+
+
+ False
+ start
+ 10
+ 10
+ vertical
+ top
+ True
+
+
+
+
+ False
+
+
+ False
+ False
+ 0
+
+
+
+
+ False
+
+
+ True
+ False
+
+
+ False
+ True
+ 0
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mcg/albumheaderbar.py b/mcg/albumheaderbar.py
new file mode 100644
index 0000000..5927a3f
--- /dev/null
+++ b/mcg/albumheaderbar.py
@@ -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()))
diff --git a/mcg/application.py b/mcg/application.py
index 0c47831..f458fa1 100644
--- a/mcg/application.py
+++ b/mcg/application.py
@@ -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'))
diff --git a/mcg/connectionpanel.py b/mcg/connectionpanel.py
new file mode 100644
index 0000000..2139d01
--- /dev/null
+++ b/mcg/connectionpanel.py
@@ -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(),)
diff --git a/mcg/coverpanel.py b/mcg/coverpanel.py
new file mode 100644
index 0000000..14da478
--- /dev/null
+++ b/mcg/coverpanel.py
@@ -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
+ )
diff --git a/mcg/infodialog.py b/mcg/infodialog.py
new file mode 100644
index 0000000..5fa3e81
--- /dev/null
+++ b/mcg/infodialog.py
@@ -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__)
diff --git a/mcg/librarypanel.py b/mcg/librarypanel.py
new file mode 100644
index 0000000..eccb846
--- /dev/null
+++ b/mcg/librarypanel.py
@@ -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
+ )
+
+
+
+
diff --git a/mcg/mcg.py b/mcg/mcg.py
index 7e5913c..e449755 100755
--- a/mcg/mcg.py
+++ b/mcg/mcg.py
@@ -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())
diff --git a/mcg/playlistpanel.py b/mcg/playlistpanel.py
new file mode 100644
index 0000000..4210efc
--- /dev/null
+++ b/mcg/playlistpanel.py
@@ -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
+ )
+
+
+
+
diff --git a/mcg/serverpanel.py b/mcg/serverpanel.py
new file mode 100644
index 0000000..0876dc3
--- /dev/null
+++ b/mcg/serverpanel.py
@@ -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())
diff --git a/mcg/shortcutsdialog.py b/mcg/shortcutsdialog.py
new file mode 100644
index 0000000..197400c
--- /dev/null
+++ b/mcg/shortcutsdialog.py
@@ -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__()
diff --git a/mcg/utils.py b/mcg/utils.py
index 0cc53ec..12a3ce4 100644
--- a/mcg/utils.py
+++ b/mcg/utils.py
@@ -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):
diff --git a/mcg/widgets.py b/mcg/widgets.py
deleted file mode 100644
index a0197b5..0000000
--- a/mcg/widgets.py
+++ /dev/null
@@ -1,2150 +0,0 @@
-#!/usr/bin/env python3
-
-
-import gi
-gi.require_version('Gtk', '3.0')
-try:
- import keyring
- use_keyring = True
-except:
- use_keyring = False
-import gettext
-import logging
-import math
-import sys
-import threading
-
-from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib, Gio
-
-from mcg import client
-from mcg.utils import SortOrder
-from mcg.utils import Utils
-from mcg.zeroconf import ZeroconfProvider
-
-
-
-
-class ShortcutsDialog():
-
-
- def __init__(self, builder, window):
- # Widgets
- self._window = builder.get_object('shortcuts-dialog')
- self._window.set_transient_for(window.get())
-
-
- def get(self):
- return self._window
-
-
- def present(self):
- self._window.present()
-
-
-
-
-class InfoDialog():
-
-
- def __init__(self, builder):
- self._logger = logging.getLogger(__name__)
-
- # Widgets
- self._info_dialog = builder.get_object('info-dialog')
- self._resize_logo()
-
-
- def get(self):
- return self._info_dialog
-
-
- def run(self):
- self._info_dialog.run()
- self._info_dialog.hide()
-
-
- def _resize_logo(self):
- try:
- logo_pixbuf = self._info_dialog.get_logo()
- self._info_dialog.set_logo(
- logo_pixbuf.scale_simple(256, 256, GdkPixbuf.InterpType.HYPER)
- )
- except:
- self._logger.warn("Failed to resize logo")
-
-
-
-
-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):
- GObject.Object.__init__(self)
-
-
-
-
-class Window():
- 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'
- STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
- _PANEL_INDEX_SERVER = 0
- _PANEL_INDEX_COVER = 1
- _PANEL_INDEX_PLAYLIST = 2
- _PANEL_INDEX_LIBRARY = 3
- _CSS_SELECTION = 'selection'
- _CUSTOM_STARTUP_COMPLETE = 'startup-complete'
-
-
- def __init__(self, app, builder, title, settings):
- self._appwindow = builder.get_object('appwindow')
- self._appwindow.set_application(app)
- self._appwindow.set_title(title)
- self._settings = settings
- self._panels = []
- self._mcg = client.Client()
- self._logger = logging.getLogger(__name__)
- self._state = WindowState()
-
- # Login screen
- self._connection_panel = ConnectionPanel(builder)
- # Panels
- self._panels.append(ServerPanel(builder))
- self._panels.append(CoverPanel(builder))
- self._panels.append(PlaylistPanel(builder, self._mcg))
- self._panels.append(LibraryPanel(builder, self._mcg))
-
- # Widgets
- # InfoBar
- self._infobar = InfoBar(builder)
- # Stack
- self._content_stack = builder.get_object('contentstack')
- self._stack = builder.get_object('panelstack')
- # Header
- self._header_bar = HeaderBar(builder)
- # Toolbar stack
- self._toolbar_stack = builder.get_object('toolbarstack')
-
- # Properties
- self._header_bar.set_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._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
- self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_enum(Window.SETTING_SORT_ORDER))
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(self._settings.get_boolean(Window.SETTING_SORT_TYPE))
-
- # Signals
- self._header_bar.connect('stack-switched', self.on_header_bar_stack_switched)
- self._header_bar.connect('toolbar-connect', self.on_header_bar_connect)
- self._header_bar.connect('toolbar-playpause', self.on_header_bar_playpause)
- self._header_bar.connect('toolbar-set-volume', self.on_header_bar_set_volume)
- self._connection_panel.connect('connection-changed', self.on_connection_panel_connection_changed)
- self._panels[Window._PANEL_INDEX_SERVER].connect('change-output-device', self.on_server_panel_output_device_changed)
- self._panels[Window._PANEL_INDEX_COVER].connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen)
- self._panels[Window._PANEL_INDEX_COVER].connect('set-song', self.on_cover_panel_set_song)
- self._panels[Window._PANEL_INDEX_COVER].connect('albumart', self.on_cover_panel_albumart)
- self._panels[Window._PANEL_INDEX_PLAYLIST].connect('clear-playlist', self.on_playlist_panel_clear_playlist)
- self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove', self.on_playlist_panel_remove)
- self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove-multiple', self.on_playlist_panel_remove_multiple)
- self._panels[Window._PANEL_INDEX_PLAYLIST].connect('play', self.on_playlist_panel_play)
- self._panels[Window._PANEL_INDEX_PLAYLIST].connect('albumart', self.on_playlist_panel_albumart)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('update', self.on_library_panel_update)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('play', self.on_library_panel_play)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue', self.on_library_panel_queue)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue-multiple', self.on_library_panel_queue_multiple)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('item-size-changed', self.on_library_panel_item_size_changed)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-order-changed', self.on_library_panel_sort_order_changed)
- self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-type-changed', self.on_library_panel_sort_type_changed)
- self._panels[Window._PANEL_INDEX_LIBRARY].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)
-
- handlers = {
- 'on_appwindow_size_allocate': self.on_resize,
- 'on_appwindow_window_state_event': self.on_state
- }
- handlers.update(self._header_bar.get_signal_handlers())
- handlers.update(self._infobar.get_signal_handlers())
- handlers.update(self._connection_panel.get_signal_handlers())
- handlers.update(self._panels[Window._PANEL_INDEX_COVER].get_signal_handlers())
- handlers.update(self._panels[Window._PANEL_INDEX_PLAYLIST].get_signal_handlers())
- handlers.update(self._panels[Window._PANEL_INDEX_LIBRARY].get_signal_handlers())
- builder.connect_signals(handlers)
-
- # Actions
- self._appwindow.set_default_size(self._state.width, self._state.height)
- if self._state.get_property(WindowState.IS_MAXIMIZED):
- self._appwindow.maximize()
- self._appwindow.show_all()
- self._content_stack.set_visible_child(self._connection_panel.get())
- 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._appwindow.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._appwindow.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._appwindow.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._appwindow.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._appwindow.add_action(self._toggle_fullscreen_action)
-
-
- def get(self):
- return self._appwindow
-
-
- def present(self):
- self._appwindow.present()
-
-
- def on_resize(self, widget, event):
- if not self._state.get_property(WindowState.IS_MAXIMIZED):
- size = self._appwindow.get_size()
- self._state.set_property(WindowState.WIDTH, size.width)
- self._state.set_property(WindowState.HEIGHT, size.height)
-
-
- 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))
-
-
- 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._stack.set_visible_child(self._panels[int(value.get_string())].get())
-
-
- def on_menu_toggle_fullscreen(self, action, value):
- self._stack.set_visible_child(self._panels[Window._PANEL_INDEX_COVER].get())
- if not self._state.get_property(WindowState.IS_FULLSCREENED):
- self._appwindow.fullscreen()
- else:
- self._appwindow.unfullscreen()
-
-
- # HeaderBar callbacks
-
- def on_header_bar_stack_switched(self, widget):
- self._set_visible_toolbar()
- self._save_visible_panel()
- self._set_menu_visible_panel()
- for panel in self._panels:
- panel.set_selected(panel.get() == self._stack.get_visible_child())
- GObject.idle_add(
- self._stack.child_set_property,
- self._stack.get_visible_child(),
- 'needs-attention',
- False
- )
-
-
- def on_header_bar_connect(self, widget):
- self._connect()
-
-
- def on_header_bar_playpause(self, widget):
- self._mcg.playpause()
- self._mcg.get_status()
-
-
- def on_header_bar_set_volume(self, widget, volume):
- self._mcg.set_volume(volume)
-
-
- # Panel callbacks
-
- 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._appwindow.fullscreen()
- else:
- self._appwindow.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._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
- self._settings.set_int(Window.SETTING_ITEM_SIZE, self._panels[Window._PANEL_INDEX_LIBRARY].get_item_size())
-
-
- def on_library_panel_sort_order_changed(self, widget, sort_order):
- self._settings.set_enum(Window.SETTING_SORT_ORDER, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_order())
-
-
- def on_library_panel_sort_type_changed(self, widget, sort_type):
- self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].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._panels[Window._PANEL_INDEX_COVER].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._header_bar.set_play)
- GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_play, pos, time)
- self._play_action.set_state(GLib.Variant.new_boolean(True))
- elif state == 'pause' or state == 'stop':
- GObject.idle_add(self._header_bar.set_pause)
- GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_pause)
- self._play_action.set_state(GLib.Variant.new_boolean(False))
- # Volume
- GObject.idle_add(self._header_bar.set_volume, volume)
- # Status
- self._panels[Window._PANEL_INDEX_SERVER].set_status(file, audio, bitrate, error)
- # Error
- if error is None:
- self._infobar.hide()
- else:
- self._show_error(error)
-
-
- def on_mcg_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
- self._panels[Window._PANEL_INDEX_SERVER].set_stats(artists, albums, songs, dbplaytime, playtime, uptime)
-
-
- def on_mcg_load_output_devices(self, devices):
- self._panels[Window._PANEL_INDEX_SERVER].set_output_devices(devices)
-
-
- def on_mcg_load_playlist(self, playlist):
- self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._connection_panel.get_host(), playlist)
-
-
- def on_mcg_init_albums(self):
- GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].init_albums)
-
-
- def on_mcg_pulse_albums(self):
- GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].load_albums)
-
-
- def on_mcg_load_albums(self, albums):
- self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._connection_panel.get_host(), albums)
-
-
- def on_mcg_load_albumart(self, album, data):
- GObject.idle_add(self._panels[self._PANEL_INDEX_COVER].set_albumart, album, data)
- GObject.idle_add(self._panels[self._PANEL_INDEX_PLAYLIST].set_albumart, album, data)
- GObject.idle_add(self._panels[self._PANEL_INDEX_LIBRARY].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._stack.child_set_property,
- panel.get(),
- '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._stack.set_visible_child(self._panels[panel_index].get())
-
-
- def on_settings_item_size_changed(self, settings, key):
- size = settings.get_int(key)
- self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
- self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(size)
-
-
- def on_settings_sort_order_changed(self, settings, key):
- sort_order = settings.get_enum(key)
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(sort_order)
-
-
- def on_settings_sort_type_changed(self, settings, key):
- sort_type = settings.get_boolean(key)
- self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(sort_type)
-
-
- # Private methods
-
- def _connect(self):
- self._connection_panel.get().set_sensitive(False)
- self._header_bar.set_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._header_bar.connected()
- self._header_bar.set_sensitive(True, False)
- self._content_stack.set_visible_child(self._stack)
- self._stack.set_visible_child(self._panels[self._settings.get_int(Window.SETTING_PANEL)].get())
-
-
- def _connect_disconnected(self):
- self._panels[Window._PANEL_INDEX_PLAYLIST].stop_threads();
- self._panels[Window._PANEL_INDEX_LIBRARY].stop_threads();
- self._header_bar.disconnected()
- self._header_bar.set_sensitive(False, False)
- self._save_visible_panel()
- self._content_stack.set_visible_child(self._connection_panel.get())
- self._connection_panel.get().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._header_bar.get().hide()
- self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(True)
- else:
- self._header_bar.get().show()
- self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(False)
-
-
- def _save_visible_panel(self):
- panels = [panel.get() for panel in self._panels]
- panel_index_selected = panels.index(self._stack.get_visible_child())
- self._settings.set_int(Window.SETTING_PANEL, panel_index_selected)
-
-
- def _set_menu_visible_panel(self):
- panels = [panel.get() for panel in self._panels]
- panel_index_selected = panels.index(self._stack.get_visible_child())
- self._panel_action.set_state(GLib.Variant.new_string(str(panel_index_selected)))
-
-
- def _set_visible_toolbar(self):
- panels = [panel.get() for panel in self._panels]
- panel_index_selected = panels.index(self._stack.get_visible_child())
- toolbar = self._panels[panel_index_selected].get_toolbar()
- self._toolbar_stack.set_visible_child(toolbar)
-
-
- def _show_error(self, message):
- self._infobar.show_error(message)
-
-
-
-
-class HeaderBar(GObject.GObject):
- __gsignals__ = {
- 'stack-switched': (GObject.SIGNAL_RUN_FIRST, None, ()),
- 'toolbar-connect': (GObject.SIGNAL_RUN_FIRST, None, ()),
- 'toolbar-playpause': (GObject.SIGNAL_RUN_FIRST, None, ()),
- 'toolbar-set-volume': (GObject.SIGNAL_RUN_FIRST, None, (int,))
- }
-
-
- def __init__(self, builder):
- GObject.GObject.__init__(self)
- self._changing_volume = False
- self._setting_volume = False
-
- # Widgets
- self._header_bar = builder.get_object('headerbar')
- self._title_stack = builder.get_object('headerbar-title-stack')
- self._connection_label = builder.get_object('headerbar-connectionn-label')
- self._stack_switcher = StackSwitcher(builder)
- self._button_connect = builder.get_object('headerbar-connection')
- self._button_playpause = builder.get_object('headerbar-playpause')
- self._button_volume = builder.get_object('headerbar-volume')
-
- # Signals
- self._stack_switcher.connect('stack-switched', self.on_stack_switched)
- self._button_handlers = {
- 'on_headerbar-connection_active_notify': self.on_connection_active_notify,
- 'on_headerbar-connection_state_set': self.on_connection_state_set,
- 'on_headerbar-playpause_toggled': self.on_playpause_toggled,
- 'on_headerbar-volume_value_changed': self.on_volume_changed,
- 'on_headerbar-volume_button_press_event': self.on_volume_press,
- 'on_headerbar-volume_button_release_event': self.on_volume_release
- }
-
-
- def get(self):
- return self._header_bar
-
-
- def get_signal_handlers(self):
- return self._button_handlers
-
-
- def set_sensitive(self, sensitive, connecting):
- self._button_playpause.set_sensitive(sensitive)
- self._button_volume.set_sensitive(sensitive)
- self._stack_switcher.get().set_sensitive(sensitive)
- self._button_connect.set_sensitive(not connecting)
-
-
- def on_connection_active_notify(self, widget, status):
- self.emit('toolbar-connect')
-
-
- def on_connection_state_set(self, widget, state):
- return True
-
-
- def on_playpause_toggled(self, widget):
- self.emit('toolbar-playpause')
-
-
- def on_stack_switched(self, widget):
- self.emit('stack-switched')
-
-
- def on_volume_changed(self, widget, value):
- if not self._setting_volume:
- self.emit('toolbar-set-volume', int(value*100))
-
-
- def on_volume_press(self, *args):
- self.volume_set_active(None, None, True)
-
-
- def on_volume_release(self, *args):
- self.volume_set_active(None, None, False)
-
-
- def volume_set_active(self, widget, event, active):
- self._changing_volume = active
-
-
- def connected(self):
- self._button_connect.handler_block_by_func(
- self.on_connection_active_notify
- )
- self._button_connect.set_active(True)
- self._button_connect.set_state(True)
- self._button_connect.handler_unblock_by_func(
- self.on_connection_active_notify
- )
- self._title_stack.set_visible_child(self._stack_switcher.get())
-
-
- def disconnected(self):
- self._button_connect.handler_block_by_func(
- self.on_connection_active_notify
- )
- self._button_connect.set_active(False)
- self._button_connect.set_state(False)
- self._button_connect.handler_unblock_by_func(
- self.on_connection_active_notify
- )
- self._title_stack.set_visible_child(self._connection_label)
-
-
- def set_play(self):
- self._button_playpause.handler_block_by_func(
- self.on_playpause_toggled
- )
- self._button_playpause.set_active(True)
- self._button_playpause.handler_unblock_by_func(
- self.on_playpause_toggled
- )
-
-
- def set_pause(self):
- self._button_playpause.handler_block_by_func(
- self.on_playpause_toggled
- )
- self._button_playpause.set_active(False)
- self._button_playpause.handler_unblock_by_func(
- self.on_playpause_toggled
- )
-
-
- def set_volume(self, volume):
- if volume >= 0:
- self._button_volume.set_visible(True)
- if not self._changing_volume:
- self._setting_volume = True
- self._button_volume.set_value(volume / 100)
- self._setting_volume = False
- else:
- self._button_volume.set_visible(False)
-
-
-
-
-class InfoBar():
- def __init__(self, builder):
- # Widgets
- self._revealer = builder.get_object('server-info-revealer')
- self._bar = builder.get_object('server-info-bar')
- self._message_label = builder.get_object('server-info-label')
-
-
- def get_signal_handlers(self):
- return {
- 'on_server-info-bar_close': self.on_close,
- 'on_server-info-bar_response': self.on_response
- }
-
-
- def on_close(self, *args):
- self.hide()
-
-
- def on_response(self, widget, response):
- self.hide()
-
-
- def hide(self):
- self._revealer.set_reveal_child(False)
-
-
- def show_error(self, message):
- self._bar.set_message_type(Gtk.MessageType.ERROR)
- self._message_label.set_text(message)
- self._revealer.set_reveal_child(True)
-
-
-
-
-class ConnectionPanel(GObject.GObject):
- __gsignals__ = {
- 'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str))
- }
-
-
- def __init__(self, builder):
- GObject.GObject.__init__(self)
- self._services = Gtk.ListStore(str, str, int)
- self._profile = None
-
- # Widgets
- self._panel = builder.get_object('connection-panel')
- # Zeroconf
- self._zeroconf_list = builder.get_object('server-zeroconf-list')
- self._zeroconf_list.set_model(self._services)
- renderer = Gtk.CellRendererText()
- column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0)
- self._zeroconf_list.append_column(column)
- # Host
- self._host_entry = builder.get_object('server-host')
- # Port
- self._port_spinner = builder.get_object('server-port')
- # Passwort
- self._password_entry = builder.get_object('server-password')
-
- # Zeroconf provider
- self._zeroconf_provider = ZeroconfProvider()
- self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service)
-
-
- def get(self):
- return self._panel
-
-
- def get_signal_handlers(self):
- return {
- 'on_server-zeroconf-list-selection_changed': self.on_service_selected,
- 'on_server-zeroconf-list_focus_out_event': self.on_zeroconf_list_outfocused,
- 'on_server-host_focus_out_event': self.on_host_entry_outfocused,
- 'on_server-port_value_changed': self.on_port_spinner_value_changed,
- 'on_server-password_focus_out_event': self.on_password_entry_outfocused
- }
-
-
- def on_new_service(self, service):
- name, host, port = service
- self._services.append([name, host, port])
-
-
- 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])
-
-
- def on_zeroconf_list_outfocused(self, widget, event):
- self._zeroconf_list.get_selection().unselect_all()
-
-
- def on_host_entry_outfocused(self, widget, event):
- self._call_back()
-
-
- def on_port_spinner_value_changed(self, widget):
- self._call_back()
-
-
- 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(),)
-
-
-
-
-class ServerPanel(GObject.GObject):
- __gsignals__ = {
- 'change-output-device': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,bool,)),
- }
-
-
- def __init__(self, builder):
- GObject.GObject.__init__(self)
- self._none_label = ""
- self._output_buttons = {}
- self._is_selected = False
-
- # Widgets
- self._panel = builder.get_object('server-panel')
- self._toolbar = builder.get_object('server-toolbar')
- self._stack = builder.get_object('server-stack')
-
- # Status widgets
- self._status_file = builder.get_object('server-status-file')
- self._status_audio = builder.get_object('server-status-audio')
- self._status_bitrate = builder.get_object('server-status-bitrate')
- self._status_error = builder.get_object('server-status-error')
- self._none_label = self._status_file.get_label()
-
- # Stats widgets
- self._stats_artists = builder.get_object('server-stats-artists')
- self._stats_albums = builder.get_object('server-stats-albums')
- self._stats_songs = builder.get_object('server-stats-songs')
- self._stats_dbplaytime = builder.get_object('server-stats-dbplaytime')
- self._stats_playtime = builder.get_object('server-stats-playtime')
- self._stats_uptime = builder.get_object('server-stats-uptime')
-
- # Audio ouptut devices widgets
- self._output_devices = builder.get_object('server-output-devices')
-
-
- def get(self):
- return self._panel
-
-
- 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())
-
-
-
-
-
-class CoverPanel(GObject.GObject):
- __gsignals__ = {
- 'toggle-fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()),
- 'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,)),
- 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,))
- }
-
-
- def __init__(self, builder):
- GObject.GObject.__init__(self)
-
- 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
-
- # Widgets
- self._appwindow = builder.get_object('appwindow')
- self._panel = builder.get_object('cover-panel')
- self._toolbar = builder.get_object('cover-toolbar')
- # Toolbar menu
- self._toolbar_fullscreen_button = builder.get_object('cover-toolbar-fullscreen')
- # Cover
- self._cover_stack = builder.get_object('cover-stack')
- self._cover_spinner = builder.get_object('cover-spinner')
- self._cover_scroll = builder.get_object('cover-scroll')
- self._cover_box = builder.get_object('cover-box')
- self._cover_image = builder.get_object('cover-image')
- self._cover_stack.set_visible_child(self._cover_scroll)
- self._cover_pixbuf = self._get_default_image()
- # Album Infos
- self._cover_info_scroll = builder.get_object('cover-info-scroll')
- self._info_revealer = builder.get_object('cover-info-revealer')
- self._info_box = builder.get_object('cover-info-box')
- self._album_title_label = builder.get_object('cover-album')
- self._album_date_label = builder.get_object('cover-date')
- self._album_artist_label = builder.get_object('cover-artist')
- # Songs
- self._songs_scale = builder.get_object('cover-songs')
-
- # Initial actions
- GObject.idle_add(self._enable_tracklist)
-
-
- def get(self):
- return self._panel
-
-
- def set_selected(self, selected):
- self._is_selected = selected
-
-
- def get_toolbar(self):
- return self._toolbar
-
-
- def get_signal_handlers(self):
- return {
- 'on_cover-box_button_press_event': self.on_cover_box_pressed,
- 'on_cover-scroll_size_allocate': self.on_cover_size_allocate,
- 'on_cover-songs_button_press_event': self.on_songs_start_change,
- 'on_cover-songs_button_release_event': self.on_songs_change
- }
-
-
- def on_cover_box_pressed(self, widget, event):
- if self._current_album and event.type == Gdk.EventType._2BUTTON_PRESS:
- self.emit('toggle-fullscreen')
-
-
- 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)
-
-
- def on_songs_start_change(self, widget, event):
- if self._timer:
- GObject.source_remove(self._timer)
- self._timer = None
-
-
- 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_fullscreen_button.set_sensitive(self._current_album is not None)
-
-
- def set_play(self, pos, time):
- if self._timer is not None:
- GObject.source_remove(self._timer)
- self._timer = None
- tracks = self._current_album.get_tracks()
- for index in range(0, pos):
- time = time + tracks[index].get_length()
-
- self._songs_scale.set_value(time+1)
- self._timer = GObject.timeout_add(1000, self._playing)
-
-
- def set_pause(self):
- if self._timer is not None:
- GObject.source_remove(self._timer)
- self._timer = None
-
-
- def set_fullscreen(self, active):
- if active:
- self._info_revealer.set_reveal_child(False)
- self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
- GObject.idle_add(self._resize_image)
- # Hide curser
- self._appwindow.get_window().set_cursor(
- Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "none")
- )
- 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)
- # Reset cursor
- self._appwindow.get_window().set_cursor(
- Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "default")
- )
-
-
- 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(
- Window.STOCK_ICON_DEFAULT,
- 512,
- Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
- )
-
-
-
-
-class PlaylistPanel(GObject.GObject):
- __gsignals__ = {
- 'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()),
- 'remove': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
- 'remove-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
- 'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
- 'albumart': (GObject.SIGNAL_RUN_FIRST, None, (str,))
- }
-
-
- def __init__(self, builder, client):
- GObject.GObject.__init__(self)
- self._logger = logging.getLogger(__name__)
- 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._appwindow = builder.get_object('appwindow')
- self._panel = builder.get_object('playlist-panel')
- self._toolbar = builder.get_object('playlist-toolbar')
- self._headerbar = builder.get_object('headerbar')
- self._headerbar_standalone = builder.get_object('headerbar-playlist-standalone')
- self._panel_normal = builder.get_object('playlist-panel-normal')
- self._panel_standalone = builder.get_object('playlist-panel-standalone')
- self._actionbar_revealer = builder.get_object('playlist-actionbar-revealer')
-
- # Select button
- self._select_button = builder.get_object('playlist-toolbar-select')
- # Clear button
- self._playlist_clear_button = builder.get_object('playlist-toolbar-clear')
- # Playlist Grid: Model
- self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
- # Playlist Grid
- self._playlist_grid = builder.get_object('playlist-iconview')
- 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)
- # Action bar (normal)
- actionbar = builder.get_object('playlist-actionbar')
- cancel_button = Gtk.Button(gettext.gettext("cancel"))
- cancel_button.connect('clicked', self.on_selection_cancel_clicked)
- actionbar.pack_start(cancel_button)
- remove_button = Gtk.Button('remove')
- remove_button.connect('clicked', self.on_selection_remove_clicked)
- actionbar.pack_end(remove_button)
-
- # Standalone labels
- self._standalone_title = builder.get_object('headerbar-playlist-standalone-title')
- self._standalone_artist = builder.get_object('headerbar-playlist-standalone-artist')
- # Standalone Image
- self._standalone_stack = builder.get_object('playlist-standalone-stack')
- self._standalone_spinner = builder.get_object('playlist-standalone-spinner')
- self._standalone_scroll = builder.get_object('playlist-standalone-scroll')
- self._standalone_image = builder.get_object('playlist-standalone-image')
- # Action bar (standalone)
- actionbar_standalone = builder.get_object('playlist-standalone-actionbar')
- play_button = Gtk.Button(gettext.gettext("play"))
- play_button.connect('clicked', self.on_standalone_play_clicked)
- actionbar_standalone.pack_end(play_button)
- remove_button = Gtk.Button(gettext.gettext("remove"))
- remove_button.connect('clicked', self.on_standalone_remove_clicked)
- actionbar_standalone.pack_end(remove_button)
-
-
- def get(self):
- return self._panel
-
-
- def set_selected(self, selected):
- self._is_selected = selected
-
-
- def get_toolbar(self):
- return self._toolbar
-
-
- def get_signal_handlers(self):
- return {
- 'on_playlist-toolbar-select_toggled': self.on_select_toggled,
- 'on_playlist-toolbar-clear_clicked': self.clear_clicked,
- 'on_playlist-iconview_item_activated': self.on_playlist_grid_clicked,
- 'on_playlist-iconview_selection_changed': self.on_playlist_grid_selection_changed,
- 'on_playlist-standalone-scroll_size_allocate': self.on_standalone_scroll_size_allocate,
- 'on_headerbar-playlist-standalone-close_clicked': self.on_standalone_close_clicked
- }
-
-
- def on_select_toggled(self, widget):
- if widget.get_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(Window._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(Window._CSS_SELECTION)
-
-
- def clear_clicked(self, widget):
- if widget is self._playlist_clear_button:
- self.emit('clear-playlist')
-
-
- 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._standalone_title.set_text(album.get_title())
- self._standalone_artist.set_text(", ".join(album.get_albumartists()))
-
- # Show panel
- self._open_standalone()
-
- # Set cover loading indicator
- self._standalone_stack.set_visible_child(self._standalone_spinner)
- self._standalone_spinner.start()
-
-
- 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])
-
-
- def on_selection_cancel_clicked(self, widget):
- self._select_button.set_active(False)
-
-
- def on_selection_remove_clicked(self, widget):
- self.emit('remove-multiple', self._selected_albums)
- self._select_button.set_active(False)
-
-
- def on_standalone_scroll_size_allocate(self, widget, allocation):
- self._resize_standalone_image()
-
-
- def on_standalone_close_clicked(self, widget):
- self._close_standalone()
-
-
- def on_standalone_remove_clicked(self, widget):
- self.emit('remove', self._selected_albums[0])
- self._close_standalone()
-
-
- 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().get_parent().child_set_property,
- self.get(),
- '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(
- Window.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._panel.set_visible_child(self._panel_standalone)
- self._appwindow.set_titlebar(self._headerbar_standalone)
-
-
- def _close_standalone(self):
- self._panel.set_visible_child(self._panel.get_children()[0])
- self._appwindow.set_titlebar(self._headerbar)
-
-
- 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(
- Window.STOCK_ICON_DEFAULT,
- 512,
- Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
- )
-
-
-
-
-class LibraryPanel(GObject.GObject):
- __gsignals__ = {
- '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,))
- }
-
-
- def __init__(self, builder, client):
- GObject.GObject.__init__(self)
- 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._appwindow = builder.get_object('appwindow')
- self._panel = builder.get_object('library-panel')
- self._toolbar = builder.get_object('library-toolbar')
- self._headerbar = builder.get_object('headerbar')
- self._headerbar_standalone = builder.get_object('headerbar-library-standalone')
- self._panel_normal = builder.get_object('library-panel-normal')
- self._panel_standalone = builder.get_object('library-panel-standalone')
- self._actionbar_revealer = builder.get_object('library-actionbar-revealer')
-
- # Select button
- self._select_button = builder.get_object('library-toolbar-select')
- # Filter/search bar
- self._filter_bar = builder.get_object('library-filter-bar')
- self._filter_entry = builder.get_object('library-filter')
- # Progress Bar
- self._stack = builder.get_object('library-stack')
- self._progress_box = builder.get_object('library-progress-box')
- self._pgross_image = builder.get_object('library-progress-image')
- self._pgross_image.set_from_pixbuf(self._get_default_image())
- self._progress_bar = builder.get_object('library-progress')
- self._scroll = builder.get_object('library-scroll')
- # Toolbar menu
- self._toolbar_search_bar = builder.get_object('library-toolbar-search')
- self._toolbar_popover = builder.get_object('library-toolbar-popover')
- self._toolbar_sort_buttons = {
- SortOrder.ARTIST: builder.get_object('library-toolbar-sort-artist'),
- SortOrder.TITLE: builder.get_object('library-toolbar-sort-title'),
- SortOrder.YEAR: builder.get_object('library-toolbar-sort-year')
- }
- self._toolbar_sort_order_button = builder.get_object('library-toolbar-sort-order')
- self._grid_scale = builder.get_object('library-toolbar-scale')
- self._grid_scale.set_value(self._item_size)
- self._grid_adjustment = builder.get_object('library-scale-adjustment')
- # 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 = builder.get_object('library-iconview')
- 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)
- # Action bar (normal)
- actionbar = builder.get_object('library-actionbar')
- cancel_button = Gtk.Button(gettext.gettext("cancel"))
- cancel_button.connect('clicked', self.on_selection_cancel_clicked)
- actionbar.pack_start(cancel_button)
- add_button = Gtk.Button(gettext.gettext("queue"))
- add_button.connect('clicked', self.on_selection_add_clicked)
- actionbar.pack_end(add_button)
-
- # Standalone labels
- self._standalone_title = builder.get_object('headerbar-library-standalone-title')
- self._standalone_artist = builder.get_object('headerbar-library-standalone-artist')
- # Standalone Image
- self._standalone_stack = builder.get_object('library-standalone-stack')
- self._standalone_spinner = builder.get_object('library-standalone-spinner')
- self._standalone_scroll = builder.get_object('library-standalone-scroll')
- self._standalone_image = builder.get_object('library-standalone-image')
- # Action bar (standalone)
- actionbar_standalone = builder.get_object('library-standalone-actionbar')
- play_button = Gtk.Button(gettext.gettext("play"))
- play_button.connect('clicked', self.on_standalone_play_clicked)
- actionbar_standalone.pack_end(play_button)
- queue_button = Gtk.Button(gettext.gettext("queue"))
- queue_button.connect('clicked', self.on_standalone_queue_clicked)
- actionbar_standalone.pack_end(queue_button)
-
-
- def get(self):
- return self._panel
-
-
- def set_selected(self, selected):
- self._is_selected = selected
-
-
- def get_toolbar(self):
- return self._toolbar
-
-
- def get_signal_handlers(self):
- return {
- 'on_library-toolbar-search_toggled': self.on_search_toggled,
- 'on_library-toolbar-select_toggled': self.on_select_toggled,
- 'on_library-toolbar-scale_change_value': self.on_grid_scale_change,
- 'on_library-toolbar-scale_button_release_event': self.on_grid_scale_changed,
- 'on_library-toolbar-update_clicked': self.on_update_clicked,
- 'on_library-toolbar-sort-toggled': self.on_sort_toggled,
- 'on_library-toolbar-sort-order_toggled': self.on_sort_order_toggled,
- 'on_library-filter-bar_notify': self.on_filter_bar_notify,
- 'on_library-filter_search_changed': self.on_filter_entry_changed,
- 'on_library-iconview_item_activated': self.on_library_grid_clicked,
- 'on_library-iconview_selection_changed': self.on_library_grid_selection_changed,
- 'on_library-standalone-scroll_size_allocate': self.on_standalone_scroll_size_allocate,
- 'on_headerbar-library-standalone-close_clicked': self.on_standalone_close_clicked,
- 'on_library-iconview_size_allocate': self.on_resize
- }
-
-
- 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._grid_scale.clear_marks()
- width = widget.get_allocation().width
-
- lower = int(self._grid_adjustment.get_lower())
- upper = int(self._grid_adjustment.get_upper())
- countMin = max(int(width / upper), 1)
- countMax = max(int(width / lower), 1)
- for index in range(countMin, countMax):
- pixel = int(width / index)
- pixel = pixel - (2 * int(pixel / 100))
- self._grid_scale.add_mark(
- pixel,
- Gtk.PositionType.BOTTOM,
- None
- )
-
-
- def on_search_toggled(self, widget):
- self._filter_bar.set_search_mode(widget.get_active())
-
-
- def on_select_toggled(self, widget):
- if widget.get_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(Window._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(Window._CSS_SELECTION)
-
-
- def on_grid_scale_change(self, widget, scroll, value):
- size = math.floor(value)
- range = self._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_grid_scale_changed(self, widget, event):
- size = round(self._grid_scale.get_value())
- range = self._grid_scale.get_adjustment()
- if size < range.get_lower() or size > range.get_upper():
- return False
- self.emit('item-size-changed', size)
- self._toolbar_popover.popdown()
- self._redraw()
- return False
-
-
- def on_update_clicked(self, widget):
- self.emit('update')
-
-
- 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._change_sort(sort)
-
-
- def on_sort_order_toggled(self, button):
- if button.get_active():
- sort_type = Gtk.SortType.DESCENDING
- else:
- sort_type = Gtk.SortType.ASCENDING
- self._sort_type = sort_type
- self._library_grid_model.set_sort_column_id(2, sort_type)
- self.emit('sort-type-changed', sort_type)
-
-
- def on_filter_bar_notify(self, widget, value):
- if self._toolbar_search_bar.get_active() is not self._filter_bar.get_search_mode():
- self._toolbar_search_bar.set_active(self._filter_bar.get_search_mode())
-
-
- def on_filter_entry_changed(self, widget):
- self._filter_string = self._filter_entry.get_text()
- GObject.idle_add(self._library_grid_filter.refilter)
-
-
- 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._standalone_title.set_text(album.get_title())
- self._standalone_artist.set_text(", ".join(album.get_albumartists()))
-
- # Show panel
- self._open_standalone()
-
- # Set cover loading indicator
- self._standalone_stack.set_visible_child(self._standalone_spinner)
- self._standalone_spinner.start()
-
-
- 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)
-
-
- def on_selection_cancel_clicked(self, widget):
- self._select_button.set_active(False)
-
-
- def on_selection_add_clicked(self, widget):
- ids = [album.get_id() for album in self._selected_albums]
- self.emit('queue-multiple', ids)
- self._select_button.set_active(False)
-
-
- def on_standalone_scroll_size_allocate(self, widget, allocation):
- self._resize_standalone_image()
-
-
- def on_standalone_play_clicked(self, widget):
- self.emit('play', self._selected_albums[0].get_id())
- self._close_standalone()
-
-
- 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._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().get_parent().child_set_property,
- self.get(),
- '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(
- Window.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._panel.set_visible_child(self._panel_standalone)
- self._appwindow.set_titlebar(self._headerbar_standalone)
-
-
- def _close_standalone(self):
- self._panel.set_visible_child(self._panel.get_children()[0])
- self._appwindow.set_titlebar(self._headerbar)
-
-
- 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(
- Window.STOCK_ICON_DEFAULT,
- 512,
- Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
- )
-
-
-
-
-class StackSwitcher(GObject.GObject):
- __gsignals__ = {
- 'stack-switched': (GObject.SIGNAL_RUN_FIRST, None, ())
- }
-
-
- def __init__(self, builder):
- GObject.GObject.__init__(self)
-
- self._temp_button = None
- self._stack_switcher = builder.get_object('header-panelswitcher')
- for child in self._stack_switcher.get_children():
- if type(child) is Gtk.RadioButton:
- child.connect('clicked', self.on_clicked)
-
-
- def on_clicked(self, widget):
- if not self._temp_button:
- self._temp_button = widget
- else:
- self._temp_button = None
- self.emit('stack-switched')
-
-
- def get(self):
- return self._stack_switcher
diff --git a/mcg/window.py b/mcg/window.py
new file mode 100644
index 0000000..e7a487d
--- /dev/null
+++ b/mcg/window.py
@@ -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)