340 lines
8.2 KiB
Python
340 lines
8.2 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
import gi, sys, os, mutagen, base64
|
||
|
|
||
|
gi.require_version('Gtk', '3.0')
|
||
|
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject
|
||
|
|
||
|
from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf, idle
|
||
|
|
||
|
from ui.image import ScaleImage
|
||
|
from ui.image_button import ImageButton
|
||
|
from ui.title_text import TitleText
|
||
|
from ui.ctrl_box import CtrlBox
|
||
|
from ui.timebar import Timebar
|
||
|
from ui.option_menu import OptionMenu
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
class SonistWindow(Gtk.Window):
|
||
|
app = None
|
||
|
mpd = None
|
||
|
stat = {}
|
||
|
|
||
|
def __init__(self, app):
|
||
|
Gtk.Window.__init__(self)
|
||
|
|
||
|
self.app = app
|
||
|
self.mpd = app.mpd
|
||
|
|
||
|
self.style_context = Gtk.StyleContext()
|
||
|
self.screen = Gdk.Screen.get_default()
|
||
|
self.css_provider = None
|
||
|
self.set_default_style()
|
||
|
|
||
|
self.connect("destroy", lambda win: app.remove_window(win))
|
||
|
|
||
|
self.mpd.connect('offline', lambda o: self.reset_player())
|
||
|
self.mpd.connect('online', lambda o: self.sync_state(None, None, True))
|
||
|
self.mpd.connect('state_changed', lambda o, stat: self.update_play_stat(stat == 'play'))
|
||
|
self.mpd.connect('song_changed', lambda o, stat, song: self.sync_state(stat, song, False))
|
||
|
self.mpd.connect('volume_changed', lambda o, vol: self.update_volume(vol))
|
||
|
self.mpd.connect('playing', lambda o, stat, song: self.update_playtime(stat))
|
||
|
|
||
|
self.set_name('SonistWindow')
|
||
|
self.set_title('Sonist')
|
||
|
self.set_default_size(320, 384)
|
||
|
self.set_resizable(False)
|
||
|
self.set_wmclass('Sonist', 'Sonist')
|
||
|
|
||
|
self.set_opacity(0.9)
|
||
|
|
||
|
album_img = f'{self.app.lib_dir}/usr/share/sonist/avatar.jpg'
|
||
|
|
||
|
self.set_background_image(album_img)
|
||
|
|
||
|
layout = Gtk.Layout()
|
||
|
|
||
|
# 菜单按钮
|
||
|
menu_btn = ImageButton(f'{self.app.lib_dir}/usr/share/sonist/menu.png')
|
||
|
popup_menu = OptionMenu(app)
|
||
|
menu_btn.connect('clicked', lambda btn: popup_menu.show(btn))
|
||
|
layout.put(menu_btn, 276, 6)
|
||
|
|
||
|
# 唱片
|
||
|
disk = ScaleImage(f'{self.app.lib_dir}/usr/share/sonist/disk.png')
|
||
|
handler = ScaleImage(f'{self.app.lib_dir}/usr/share/sonist/handler.png')
|
||
|
album = ScaleImage(album_img)
|
||
|
|
||
|
disk.resize(192, 192)
|
||
|
album.clip_resize(128).set_radius(64)
|
||
|
|
||
|
handler.resize(48, 96)
|
||
|
self.handler = handler
|
||
|
self.album = album
|
||
|
|
||
|
box = Gtk.Fixed()
|
||
|
box.put(disk, 16, 16)
|
||
|
box.put(album, 48, 48)
|
||
|
box.put(handler, 0, 16)
|
||
|
|
||
|
layout.put(box, 48, 16)
|
||
|
|
||
|
|
||
|
# title
|
||
|
self.title_box = TitleText()
|
||
|
|
||
|
layout.put(self.title_box, 27, 244)
|
||
|
|
||
|
|
||
|
# 播放进度
|
||
|
self.timebar = Timebar()
|
||
|
self.timebar.connect('seeked', lambda a,v: self.mpd.seekcur(v))
|
||
|
layout.put(self.timebar, 24, 272)
|
||
|
|
||
|
|
||
|
# 控制条
|
||
|
self.ctrl_box = CtrlBox()
|
||
|
self.ctrl_box.connect('clicked', self.ctrl_clicked)
|
||
|
self.ctrl_box.connect('volume_changed', lambda box, vol: self.mpd.setvol(vol))
|
||
|
|
||
|
|
||
|
layout.put(self.ctrl_box, 48, 312)
|
||
|
|
||
|
self.add(layout)
|
||
|
|
||
|
|
||
|
|
||
|
def set_default_style(self):
|
||
|
provider = Gtk.CssProvider()
|
||
|
css = f"""
|
||
|
#SonistWindow {{
|
||
|
background-image: url('{self.app.lib_dir}/usr/share/sonist/album.png');
|
||
|
background-size: 100% 100%;
|
||
|
background-position: center;
|
||
|
}}
|
||
|
#text {{
|
||
|
color: #f2f5fc;
|
||
|
}}
|
||
|
|
||
|
#ImageButton {{
|
||
|
border: 0;
|
||
|
border-radius: 50%;
|
||
|
background-color: transparent;
|
||
|
border-color:transparent;
|
||
|
outline: transparent;
|
||
|
}}
|
||
|
#ImageButton:hover {{
|
||
|
background-color: rgba(255,255,255,.1);
|
||
|
outline: transparent;
|
||
|
box-shadow:none;
|
||
|
}}
|
||
|
|
||
|
#Slider {{
|
||
|
outline: none;
|
||
|
}}
|
||
|
#Slider trough {{
|
||
|
background-color: rgba(129, 161, 193, 0.35);
|
||
|
outline: none;
|
||
|
}}
|
||
|
#Slider trough highlight {{
|
||
|
background-color: rgba(163, 190, 140, 0.75);
|
||
|
}}
|
||
|
#Slider slider {{
|
||
|
background-color: transparent;
|
||
|
border-color: transparent;
|
||
|
outline: none;
|
||
|
}}
|
||
|
"""
|
||
|
provider.load_from_data(css.encode('UTF-8'))
|
||
|
self.style_context.add_provider_for_screen(self.screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
def get_mpd_stat(self):
|
||
|
try:
|
||
|
self.stat = self.mpd.status()
|
||
|
except:
|
||
|
self.stat = {}
|
||
|
return self.stat
|
||
|
|
||
|
|
||
|
def set_background_image(self, filepath):
|
||
|
if type(filepath) == str:
|
||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath)
|
||
|
else:
|
||
|
pixbuf = filepath
|
||
|
|
||
|
pixbuf = blur_image(pixbuf)
|
||
|
pixbuf.savev(f"/tmp/sonist_album_cache", 'png', [], [])
|
||
|
|
||
|
css = f"""
|
||
|
#SonistWindow {{
|
||
|
background-image: url('/tmp/sonist_album_cache');
|
||
|
}}
|
||
|
"""
|
||
|
# 加载CSS样式
|
||
|
if self.css_provider is None:
|
||
|
self.css_provider = Gtk.CssProvider()
|
||
|
else:
|
||
|
self.style_context.remove_provider_for_screen(self.screen, self.css_provider)
|
||
|
|
||
|
self.css_provider.load_from_data(css.encode('UTF-8'))
|
||
|
|
||
|
self.style_context.add_provider_for_screen(self.screen, self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||
|
|
||
|
|
||
|
def ctrl_clicked(self, box, btn):
|
||
|
match(btn):
|
||
|
case 'play_btn':
|
||
|
self.toggle_play()
|
||
|
|
||
|
case 'mode_btn':
|
||
|
# repeat all
|
||
|
if self.ctrl_box.curr_mode == 0:
|
||
|
self.mpd.repeat(1)
|
||
|
self.mpd.random(0)
|
||
|
self.mpd.single(0)
|
||
|
# random
|
||
|
elif self.ctrl_box.curr_mode == 1:
|
||
|
self.mpd.repeat(0)
|
||
|
self.mpd.random(1)
|
||
|
self.mpd.single(0)
|
||
|
# single
|
||
|
else:
|
||
|
self.mpd.repeat(0)
|
||
|
self.mpd.random(0)
|
||
|
self.mpd.single(1)
|
||
|
|
||
|
case 'prev_btn':
|
||
|
self.prev_song()
|
||
|
|
||
|
case 'next_btn':
|
||
|
self.next_song()
|
||
|
|
||
|
case 'vol_btn':
|
||
|
self.toggle_play()
|
||
|
|
||
|
|
||
|
|
||
|
def toggle_play(self):
|
||
|
state = self.stat.get('state')
|
||
|
is_play = state == 'play'
|
||
|
try:
|
||
|
if state == 'stop':
|
||
|
self.mpd.play()
|
||
|
state = 'play'
|
||
|
else:
|
||
|
self.mpd.pause()
|
||
|
state = 'pause' if is_play else 'play'
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
self.stat['state'] = state
|
||
|
self.update_play_stat(state == 'play')
|
||
|
|
||
|
|
||
|
def prev_song(self):
|
||
|
try:
|
||
|
self.mpd.previous()
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
def next_song(self):
|
||
|
try:
|
||
|
self.mpd.next()
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
@idle
|
||
|
def update_play_stat(self, is_play = True):
|
||
|
|
||
|
if is_play:
|
||
|
self.handler.reset(f'{self.app.lib_dir}/usr/share/sonist/handler_a.png')
|
||
|
else:
|
||
|
self.handler.reset(f'{self.app.lib_dir}/usr/share/sonist/handler.png')
|
||
|
|
||
|
# 切换播放按钮状态
|
||
|
self.ctrl_box.toggle_play_btn(is_play)
|
||
|
|
||
|
@idle
|
||
|
def update_playtime(self, stat = {}):
|
||
|
self.stat = stat
|
||
|
times = stat['time'] or '0:0'
|
||
|
times = times.split(':')
|
||
|
self.timebar.update_time(int(times[0]), int(times[1]))
|
||
|
|
||
|
@idle
|
||
|
def update_volume(self, vol = 100):
|
||
|
self.ctrl_box.set_volume(vol)
|
||
|
|
||
|
@idle
|
||
|
def sync_state(self, stat = None, song = None, first = False):
|
||
|
self.ctrl_box.disabled = False
|
||
|
|
||
|
self.stat = stat or self.get_mpd_stat()
|
||
|
|
||
|
state = self.stat.get('state')
|
||
|
|
||
|
if first:
|
||
|
self.update_play_stat(state == 'play')
|
||
|
self.update_volume(int(self.stat.get('volume') or 100))
|
||
|
|
||
|
if self.stat.get('single') == '1':
|
||
|
self.ctrl_box.toggle_mode_btn(mode = 'single')
|
||
|
elif self.stat.get('random') == '1':
|
||
|
self.ctrl_box.toggle_mode_btn(mode = 'random')
|
||
|
|
||
|
|
||
|
if state != 'stop':
|
||
|
song = song or self.mpd.currentsong()
|
||
|
# 更新歌曲信息
|
||
|
self.title_box.update(f"{song.get('artist')} - {song.get('title')}")
|
||
|
self.update_playtime(self.stat)
|
||
|
|
||
|
title = song['file']
|
||
|
title_hex = base64.b64encode(title.encode()).hex()
|
||
|
|
||
|
filepath = f"{self.app.album_cache_dir}/{title_hex}.png"
|
||
|
songpath = f"{self.app.music_dir}/{title}"
|
||
|
|
||
|
if os.path.isfile(filepath):
|
||
|
self.update_album(filepath)
|
||
|
else:
|
||
|
|
||
|
id3 = mutagen.File(songpath)
|
||
|
pic = None
|
||
|
try:
|
||
|
if id3.tags.get('APIC:'):
|
||
|
pic = id3.tags['APIC:']
|
||
|
elif len(id3.pictures) > 0:
|
||
|
pic = id3.pictures[0]
|
||
|
|
||
|
if pic is None:
|
||
|
self.update_album()
|
||
|
else:
|
||
|
album = pic_to_pixbuf(pic)
|
||
|
self.update_album(album)
|
||
|
|
||
|
except Exception as err:
|
||
|
pass
|
||
|
|
||
|
|
||
|
|
||
|
@idle
|
||
|
def reset_player(self):
|
||
|
self.ctrl_box.disabled = True
|
||
|
self.title_box.update('mpd is offline...')
|
||
|
self.timebar.update_time()
|
||
|
self.update_album()
|
||
|
|
||
|
@idle
|
||
|
def update_album(self, filepath = './usr/share/sonist/avatar.jpg'):
|
||
|
self.set_background_image(filepath)
|
||
|
self.album.reset(filepath, True).set_radius(64)
|
||
|
|
||
|
|