Got a ton of things hooked up
This commit is contained in:
@@ -1,13 +1,11 @@
|
|||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
import threading
|
|
||||||
import argparse
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import GLib, Gio, Gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
from .ui import LibremsonicApp
|
from .app import LibremsonicApp
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import mpv
|
import mpv
|
||||||
|
|
||||||
@@ -6,10 +7,10 @@ import gi
|
|||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gio, Gtk, GLib, Gdk
|
from gi.repository import Gio, Gtk, GLib, Gdk
|
||||||
|
|
||||||
from libremsonic.config import get_config, save_config
|
from .ui.main import MainWindow
|
||||||
|
from .ui.configure_servers import ConfigureServersDialog
|
||||||
|
|
||||||
from .main import MainWindow
|
from .state_manager import ApplicationState
|
||||||
from .configure_servers import ConfigureServersDialog
|
|
||||||
|
|
||||||
|
|
||||||
class LibremsonicApp(Gtk.Application):
|
class LibremsonicApp(Gtk.Application):
|
||||||
@@ -21,8 +22,7 @@ class LibremsonicApp(Gtk.Application):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
self.window = None
|
self.window = None
|
||||||
self.config_file = None
|
self.state = ApplicationState()
|
||||||
self.config = None
|
|
||||||
|
|
||||||
# Specify Command Line Options
|
# Specify Command Line Options
|
||||||
self.add_main_option(
|
self.add_main_option(
|
||||||
@@ -30,24 +30,30 @@ class LibremsonicApp(Gtk.Application):
|
|||||||
'Specify a configuration file. Defaults to ~/.config/libremsonic/config.json',
|
'Specify a configuration file. Defaults to ~/.config/libremsonic/config.json',
|
||||||
None)
|
None)
|
||||||
|
|
||||||
self.player = mpv.MPV()
|
self.player = mpv.MPV(pause=True)
|
||||||
self.is_playing = False
|
|
||||||
|
@self.player.property_observer('time-pos')
|
||||||
|
def time_observer(_name, value):
|
||||||
|
# TODO handle this
|
||||||
|
# self.window.player_controls.update_scrubber
|
||||||
|
pass
|
||||||
|
|
||||||
# Handle command line option parsing.
|
# Handle command line option parsing.
|
||||||
def do_command_line(self, command_line):
|
def do_command_line(self, command_line):
|
||||||
options = command_line.get_options_dict()
|
options = command_line.get_options_dict()
|
||||||
|
|
||||||
# Config File
|
# Config File
|
||||||
self.config_file = options.lookup_value('config')
|
config_file = options.lookup_value('config')
|
||||||
if self.config_file:
|
if config_file:
|
||||||
self.config_file = self.config_file.get_bytestring().decode(
|
config_file = config_file.get_bytestring().decode('utf-8')
|
||||||
'utf-8')
|
|
||||||
else:
|
else:
|
||||||
# Default to ~/.config/libremsonic.
|
# Default to ~/.config/libremsonic.
|
||||||
config_folder = (os.environ.get('XDG_CONFIG_HOME')
|
config_folder = (os.environ.get('XDG_CONFIG_HOME')
|
||||||
or os.path.expanduser('~/.config'))
|
or os.path.expanduser('~/.config'))
|
||||||
config_folder = os.path.join(config_folder, 'libremsonic')
|
config_folder = os.path.join(config_folder, 'libremsonic')
|
||||||
self.config_file = os.path.join(config_folder, 'config.yaml')
|
config_file = os.path.join(config_folder, 'config.yaml')
|
||||||
|
|
||||||
|
self.state.config_file = config_file
|
||||||
|
|
||||||
# Have to do this or else the application doesn't work. Not entirely
|
# Have to do this or else the application doesn't work. Not entirely
|
||||||
# sure why, but C-bindings...
|
# sure why, but C-bindings...
|
||||||
@@ -57,66 +63,83 @@ class LibremsonicApp(Gtk.Application):
|
|||||||
def do_startup(self):
|
def do_startup(self):
|
||||||
Gtk.Application.do_startup(self)
|
Gtk.Application.do_startup(self)
|
||||||
|
|
||||||
# Add action for configuring servers
|
def add_action(name: str, fn):
|
||||||
action = Gio.SimpleAction.new('configure_servers', None)
|
"""Registers an action with the application."""
|
||||||
action.connect('activate', self.on_configure_servers)
|
action = Gio.SimpleAction.new(name, None)
|
||||||
self.add_action(action)
|
action.connect('activate', fn)
|
||||||
|
self.add_action(action)
|
||||||
|
|
||||||
# Add action for configuring servers
|
# Add action for menu items.
|
||||||
action = Gio.SimpleAction.new('play_pause', None)
|
add_action('configure-servers', self.on_configure_servers)
|
||||||
action.connect('activate', self.on_play_pause)
|
|
||||||
self.add_action(action)
|
# Add actions for player controls
|
||||||
|
add_action('play-pause', self.on_play_pause)
|
||||||
|
add_action('next-track', self.on_next_track)
|
||||||
|
add_action('prev-track', self.on_prev_track)
|
||||||
|
add_action('repeat-press', self.on_repeat_press)
|
||||||
|
add_action('shuffle-press', self.on_shuffle_press)
|
||||||
|
|
||||||
def do_activate(self):
|
def do_activate(self):
|
||||||
# We only allow a single window and raise any existing ones
|
# We only allow a single window and raise any existing ones
|
||||||
if not self.window:
|
if not self.window:
|
||||||
# Windows are associated with the application
|
# Windows are associated with the application when the last one is
|
||||||
# when the last one is closed the application shuts down
|
# closed the application shuts down.
|
||||||
self.window = MainWindow(application=self, title="LibremSonic")
|
self.window = MainWindow(application=self, title="LibremSonic")
|
||||||
|
|
||||||
|
# Configure the CSS provider so that we can style elements on the
|
||||||
|
# window.
|
||||||
css_provider = Gtk.CssProvider()
|
css_provider = Gtk.CssProvider()
|
||||||
css_provider.load_from_path(
|
css_provider.load_from_path(
|
||||||
os.path.join(os.path.dirname(__file__), 'app_styles.css'))
|
os.path.join(os.path.dirname(__file__), 'ui/app_styles.css'))
|
||||||
|
|
||||||
context = Gtk.StyleContext()
|
context = Gtk.StyleContext()
|
||||||
screen = Gdk.Screen.get_default()
|
screen = Gdk.Screen.get_default()
|
||||||
context.add_provider_for_screen(screen, css_provider,
|
context.add_provider_for_screen(screen, css_provider,
|
||||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||||
|
|
||||||
|
# Display the window.
|
||||||
self.window.show_all()
|
self.window.show_all()
|
||||||
self.window.present()
|
self.window.present()
|
||||||
|
|
||||||
# Load the configuration and update the UI with the curent server, if
|
# Load the configuration and update the UI with the curent server, if
|
||||||
# it exists. If there is no current server, show the dialog to select a
|
# it exists.
|
||||||
# server.
|
self.state.load_config()
|
||||||
self.load_config()
|
|
||||||
|
|
||||||
if self.config.current_server is None:
|
# If there is no current server, show the dialog to select a server.
|
||||||
|
if self.state.config.current_server is None:
|
||||||
self.show_configure_servers_dialog()
|
self.show_configure_servers_dialog()
|
||||||
else:
|
else:
|
||||||
self.on_connected_server_changed(None, self.config.current_server)
|
self.on_connected_server_changed(None,
|
||||||
|
self.state.config.current_server)
|
||||||
|
|
||||||
# ########## ACTION HANDLERS ########## #
|
# ########## ACTION HANDLERS ########## #
|
||||||
def on_configure_servers(self, action, param):
|
def on_configure_servers(self, action, param):
|
||||||
self.show_configure_servers_dialog()
|
self.show_configure_servers_dialog()
|
||||||
|
|
||||||
def on_play_pause(self, action, param):
|
def on_play_pause(self, action, param):
|
||||||
if self.is_playing:
|
self.player.command('cycle', 'pause')
|
||||||
self.player.command('cycle', 'pause')
|
self.state.playing = not self.state.playing
|
||||||
else:
|
|
||||||
self.player.loadfile(
|
|
||||||
'/home/sumner/Music/Sapphyre/All You See Is Christ (live).mp3')
|
|
||||||
|
|
||||||
self.is_playing = not self.is_playing
|
|
||||||
|
|
||||||
self.update_window()
|
self.update_window()
|
||||||
|
|
||||||
|
def on_next_track(self, action, params):
|
||||||
|
self.player.playlist_next()
|
||||||
|
|
||||||
|
def on_prev_track(self, action, params):
|
||||||
|
self.player.playlist_prev()
|
||||||
|
|
||||||
|
def on_repeat_press(self, action, params):
|
||||||
|
print('repeat press')
|
||||||
|
|
||||||
|
def on_shuffle_press(self, action, params):
|
||||||
|
print('shuffle press')
|
||||||
|
|
||||||
def on_server_list_changed(self, action, servers):
|
def on_server_list_changed(self, action, servers):
|
||||||
self.config.servers = servers
|
self.state.config.servers = servers
|
||||||
self.save_config()
|
self.state.save_config()
|
||||||
|
|
||||||
def on_connected_server_changed(self, action, current_server):
|
def on_connected_server_changed(self, action, current_server):
|
||||||
self.config.current_server = current_server
|
self.state.config.current_server = current_server
|
||||||
self.save_config()
|
self.state.save_config()
|
||||||
|
|
||||||
# Update the window according to the new server configuration.
|
# Update the window according to the new server configuration.
|
||||||
self.update_window()
|
self.update_window()
|
||||||
@@ -124,22 +147,12 @@ class LibremsonicApp(Gtk.Application):
|
|||||||
# ########## HELPER METHODS ########## #
|
# ########## HELPER METHODS ########## #
|
||||||
def show_configure_servers_dialog(self):
|
def show_configure_servers_dialog(self):
|
||||||
"""Show the Connect to Server dialog."""
|
"""Show the Connect to Server dialog."""
|
||||||
dialog = ConfigureServersDialog(self.window, self.config)
|
dialog = ConfigureServersDialog(self.window, self.state.config)
|
||||||
dialog.connect('server-list-changed', self.on_server_list_changed)
|
dialog.connect('server-list-changed', self.on_server_list_changed)
|
||||||
dialog.connect('connected-server-changed',
|
dialog.connect('connected-server-changed',
|
||||||
self.on_connected_server_changed)
|
self.on_connected_server_changed)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
|
||||||
def load_config(self):
|
|
||||||
self.config = get_config(self.config_file)
|
|
||||||
|
|
||||||
def save_config(self):
|
|
||||||
save_config(self.config, self.config_file)
|
|
||||||
|
|
||||||
def update_window(self):
|
def update_window(self):
|
||||||
self.window.update(
|
self.window.update(self.state)
|
||||||
server=self.config.servers[self.config.current_server],
|
|
||||||
current_song=None,
|
|
||||||
is_playing=self.is_playing,
|
|
||||||
)
|
|
19
libremsonic/state_manager.py
Normal file
19
libremsonic/state_manager.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from typing import List, Any
|
||||||
|
|
||||||
|
from .config import get_config, save_config, AppConfiguration
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationState:
|
||||||
|
config: AppConfiguration = AppConfiguration.get_default_configuration()
|
||||||
|
current_song: Any # TODO fix
|
||||||
|
config_file: str
|
||||||
|
playing: bool = False
|
||||||
|
song_progress: float = 0.0
|
||||||
|
up_next: List[Any] # TODO should be song
|
||||||
|
volume: int = 100
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
self.config = get_config(self.config_file)
|
||||||
|
|
||||||
|
def save_config(self):
|
||||||
|
save_config(self.config, self.config_file)
|
@@ -1 +0,0 @@
|
|||||||
from .app import LibremsonicApp
|
|
||||||
|
@@ -13,11 +13,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#player-controls-bar #song-scrubber {
|
#player-controls-bar #song-scrubber {
|
||||||
min-width: 250px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-controls-bar #volume-slider {
|
#player-controls-bar #volume-slider {
|
||||||
min-width: 120px;
|
min-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#player-controls-bar #volume-slider value {
|
#player-controls-bar #volume-slider value {
|
||||||
@@ -27,3 +27,7 @@
|
|||||||
#player-controls-bar #song-name {
|
#player-controls-bar #song-name {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#player-controls-bar #album-name {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
@@ -7,6 +7,7 @@ from gi.repository import Gio, Gtk
|
|||||||
from .albums import AlbumsPanel
|
from .albums import AlbumsPanel
|
||||||
from .player_controls import PlayerControls
|
from .player_controls import PlayerControls
|
||||||
from libremsonic.server import Server
|
from libremsonic.server import Server
|
||||||
|
from libremsonic.state_manager import ApplicationState
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(Gtk.ApplicationWindow):
|
class MainWindow(Gtk.ApplicationWindow):
|
||||||
@@ -38,15 +39,17 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||||||
self.add(flowbox)
|
self.add(flowbox)
|
||||||
|
|
||||||
# TODO the song should eventually be an API object...
|
# TODO the song should eventually be an API object...
|
||||||
def update(self, server: Optional[Server], current_song, is_playing):
|
def update(self, state: ApplicationState):
|
||||||
# Update the Connected to label on the popup menu.
|
# Update the Connected to label on the popup menu.
|
||||||
if server:
|
if state.config.current_server >= 0:
|
||||||
self.connected_to_label.set_markup(f'Connected to {server.name}')
|
server_name = state.config.servers[
|
||||||
|
state.config.current_server].name
|
||||||
|
self.connected_to_label.set_markup(f'Connected to {server_name}')
|
||||||
else:
|
else:
|
||||||
self.connected_to_label.set_markup(
|
self.connected_to_label.set_markup(
|
||||||
f'<span style="italic">Not Connected to a Server</span>')
|
f'<span style="italic">Not Connected to a Server</span>')
|
||||||
|
|
||||||
self.player_controls.update(current_song, is_playing)
|
self.player_controls.update(state)
|
||||||
|
|
||||||
def create_stack(self, **kwargs):
|
def create_stack(self, **kwargs):
|
||||||
stack = Gtk.Stack()
|
stack = Gtk.Stack()
|
||||||
@@ -95,7 +98,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
menu_items = [
|
menu_items = [
|
||||||
(None, self.connected_to_label),
|
(None, self.connected_to_label),
|
||||||
('app.configure_servers', Gtk.ModelButton('Connect to Server')),
|
('app.configure-servers', Gtk.ModelButton('Connect to Server')),
|
||||||
]
|
]
|
||||||
|
|
||||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
@@ -3,6 +3,8 @@ import gi
|
|||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
from gi.repository import Gio, Gtk, Gtk
|
from gi.repository import Gio, Gtk, Gtk
|
||||||
|
|
||||||
|
from libremsonic.state_manager import ApplicationState
|
||||||
|
|
||||||
|
|
||||||
class PlayerControls(Gtk.ActionBar):
|
class PlayerControls(Gtk.ActionBar):
|
||||||
"""
|
"""
|
||||||
@@ -13,18 +15,18 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
Gtk.ActionBar.__init__(self)
|
Gtk.ActionBar.__init__(self)
|
||||||
self.set_name('player-controls-bar')
|
self.set_name('player-controls-bar')
|
||||||
|
|
||||||
self.song_display = self.create_song_display()
|
song_display = self.create_song_display()
|
||||||
self.playback_controls = self.create_playback_controls()
|
playback_controls = self.create_playback_controls()
|
||||||
self.up_next_volume = self.create_up_next_volume()
|
up_next_volume = self.create_up_next_volume()
|
||||||
|
|
||||||
self.pack_start(self.song_display)
|
self.pack_start(song_display)
|
||||||
self.set_center_widget(self.playback_controls)
|
self.set_center_widget(playback_controls)
|
||||||
self.pack_end(self.up_next_volume)
|
self.pack_end(up_next_volume)
|
||||||
|
|
||||||
def update(self, current_song, playing):
|
def update(self, state: ApplicationState):
|
||||||
|
icon = 'pause' if state.playing else 'start'
|
||||||
self.play_button.get_child().set_from_icon_name(
|
self.play_button.get_child().set_from_icon_name(
|
||||||
f"media-playback-{'start' if not playing else 'pause'}-symbolic",
|
f"media-playback-{icon}-symbolic", Gtk.IconSize.LARGE_TOOLBAR)
|
||||||
Gtk.IconSize.LARGE_TOOLBAR)
|
|
||||||
|
|
||||||
def create_song_display(self):
|
def create_song_display(self):
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
@@ -42,8 +44,15 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
details_box.add(self.song_name)
|
details_box.add(self.song_name)
|
||||||
|
|
||||||
self.album_name = Gtk.Label('Album name', halign=Gtk.Align.START)
|
self.album_name = Gtk.Label('Album name', halign=Gtk.Align.START)
|
||||||
|
self.album_name.set_name('album-name')
|
||||||
details_box.add(self.album_name)
|
details_box.add(self.album_name)
|
||||||
|
|
||||||
|
self.artist_name = Gtk.Label('<i>Artist name</i>',
|
||||||
|
halign=Gtk.Align.START,
|
||||||
|
use_markup=True)
|
||||||
|
self.artist_name.set_name('artist-name')
|
||||||
|
details_box.add(self.artist_name)
|
||||||
|
|
||||||
details_box.pack_start(Gtk.Box(), True, True, 0)
|
details_box.pack_start(Gtk.Box(), True, True, 0)
|
||||||
box.pack_start(details_box, True, True, 5)
|
box.pack_start(details_box, True, True, 5)
|
||||||
|
|
||||||
@@ -64,8 +73,8 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.song_scrubber.set_draw_value(False)
|
self.song_scrubber.set_draw_value(False)
|
||||||
scrubber_box.pack_start(self.song_scrubber, True, True, 0)
|
scrubber_box.pack_start(self.song_scrubber, True, True, 0)
|
||||||
|
|
||||||
self.song_lengh_label = Gtk.Label('0:00')
|
self.song_length_label = Gtk.Label('0:00')
|
||||||
scrubber_box.pack_start(self.song_lengh_label, False, False, 5)
|
scrubber_box.pack_start(self.song_length_label, False, False, 5)
|
||||||
|
|
||||||
box.add(scrubber_box)
|
box.add(scrubber_box)
|
||||||
|
|
||||||
@@ -75,12 +84,14 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
# Repeat button
|
# Repeat button
|
||||||
self.repeat_button = self.button_with_icon(
|
self.repeat_button = self.button_with_icon(
|
||||||
'media-playlist-repeat-symbolic')
|
'media-playlist-repeat-symbolic')
|
||||||
|
self.repeat_button.set_action_name('app.repeat-press')
|
||||||
buttons.pack_start(self.repeat_button, False, False, 5)
|
buttons.pack_start(self.repeat_button, False, False, 5)
|
||||||
|
|
||||||
# Previous button
|
# Previous button
|
||||||
previous_button = self.button_with_icon(
|
previous_button = self.button_with_icon(
|
||||||
'media-skip-backward-symbolic',
|
'media-skip-backward-symbolic',
|
||||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||||
|
previous_button.set_action_name('app.prev-track')
|
||||||
buttons.pack_start(previous_button, False, False, 5)
|
buttons.pack_start(previous_button, False, False, 5)
|
||||||
|
|
||||||
# Play button
|
# Play button
|
||||||
@@ -89,18 +100,20 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
relief=True,
|
relief=True,
|
||||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||||
self.play_button.set_name('play-button')
|
self.play_button.set_name('play-button')
|
||||||
self.play_button.set_action_name('app.play_pause')
|
self.play_button.set_action_name('app.play-pause')
|
||||||
buttons.pack_start(self.play_button, False, False, 0)
|
buttons.pack_start(self.play_button, False, False, 0)
|
||||||
|
|
||||||
# Next button
|
# Next button
|
||||||
next_button = self.button_with_icon(
|
next_button = self.button_with_icon(
|
||||||
'media-skip-forward-symbolic',
|
'media-skip-forward-symbolic',
|
||||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||||
|
next_button.set_action_name('app.next-track')
|
||||||
buttons.pack_start(next_button, False, False, 5)
|
buttons.pack_start(next_button, False, False, 5)
|
||||||
|
|
||||||
# Shuffle button
|
# Shuffle button
|
||||||
self.shuffle_button = self.button_with_icon(
|
self.shuffle_button = self.button_with_icon(
|
||||||
'media-playlist-shuffle-symbolic')
|
'media-playlist-shuffle-symbolic')
|
||||||
|
self.shuffle_button.set_action_name('app.shuffle-press')
|
||||||
buttons.pack_start(self.shuffle_button, False, False, 5)
|
buttons.pack_start(self.shuffle_button, False, False, 5)
|
||||||
|
|
||||||
buttons.pack_start(Gtk.Box(), True, True, 0)
|
buttons.pack_start(Gtk.Box(), True, True, 0)
|
||||||
@@ -127,7 +140,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
volume_slider = Gtk.Scale.new_with_range(
|
volume_slider = Gtk.Scale.new_with_range(
|
||||||
orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=5)
|
orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=5)
|
||||||
volume_slider.set_name('volume-slider')
|
volume_slider.set_name('volume-slider')
|
||||||
volume_slider.set_value_pos(Gtk.PositionType.RIGHT)
|
volume_slider.set_draw_value(False)
|
||||||
volume_slider.set_value(100)
|
volume_slider.set_value(100)
|
||||||
box.pack_start(volume_slider, True, True, 0)
|
box.pack_start(volume_slider, True, True, 0)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user