sonist-gtk/usr/lib/sonist/window.py

350 lines
8.4 KiB
Python
Raw Normal View History

2023-08-17 00:06:21 +08:00
#!/usr/bin/env python3
2023-08-24 17:04:41 +08:00
import gi, sys, os, mutagen, base64
2023-08-17 00:06:21 +08:00
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject
2023-08-17 00:06:21 +08:00
2023-08-24 17:04:41 +08:00
from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf, idle
2023-08-25 16:50:16 +08:00
from assets import image_dict
2023-08-17 20:51:59 +08:00
from ui.image import ScaleImage
from ui.image_button import ImageButton
2023-08-24 17:04:41 +08:00
from ui.title_text import TitleText
2023-08-18 19:04:51 +08:00
from ui.ctrl_box import CtrlBox
from ui.timebar import Timebar
2023-08-22 18:52:18 +08:00
from ui.option_menu import OptionMenu
2023-08-28 10:53:11 +08:00
from ui.disk_box import DiskBox
2023-08-17 20:51:59 +08:00
2023-08-24 17:04:41 +08:00
2023-08-17 20:51:59 +08:00
2023-08-17 00:06:21 +08:00
class SonistWindow(Gtk.Window):
2023-08-23 20:36:52 +08:00
app = None
mpd = None
stat = {}
2023-08-18 19:04:51 +08:00
def __init__(self, app):
2023-08-17 00:06:21 +08:00
Gtk.Window.__init__(self)
2023-08-18 19:04:51 +08:00
self.app = app
2023-08-23 20:36:52 +08:00
self.mpd = app.mpd
2023-08-18 19:04:51 +08:00
2023-08-24 18:21:45 +08:00
self.style_context = Gtk.StyleContext()
self.screen = Gdk.Screen.get_default()
self.css_provider = None
self.set_default_style()
self.set_decorated(False)
2023-08-17 20:51:59 +08:00
self.set_name('SonistWindow')
2023-08-24 21:47:40 +08:00
self.set_title('Sonist')
2023-08-17 20:51:59 +08:00
self.set_default_size(320, 384)
2023-08-18 19:04:51 +08:00
self.set_resizable(False)
2023-08-17 20:51:59 +08:00
self.set_wmclass('Sonist', 'Sonist')
self.set_opacity(0.9)
2023-08-25 18:33:01 +08:00
self.set_background_image(image_dict['disk'])
2023-08-17 20:51:59 +08:00
2023-08-28 10:53:11 +08:00
self.connect("destroy", lambda win: app.remove_window(win))
# self.connect("button-press-event", self.on_drag_start)
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))
2023-08-17 20:51:59 +08:00
layout = Gtk.Layout()
2023-08-18 00:09:43 +08:00
# 菜单按钮
2023-08-25 16:50:16 +08:00
menu_btn = ImageButton(image_dict['menu'])
2023-08-24 15:25:03 +08:00
popup_menu = OptionMenu(app)
2023-08-24 18:21:45 +08:00
menu_btn.connect('clicked', lambda btn: popup_menu.show(btn))
2023-08-22 18:52:18 +08:00
layout.put(menu_btn, 276, 6)
2023-08-17 20:51:59 +08:00
# 唱片
2023-08-28 10:53:11 +08:00
disk = DiskBox()
self.disk = disk
layout.put(disk, 48, 16)
2023-08-17 20:51:59 +08:00
# title
2023-08-24 17:04:41 +08:00
self.title_box = TitleText()
layout.put(self.title_box, 27, 244)
2023-08-17 20:51:59 +08:00
# 播放进度
self.timebar = Timebar()
2023-08-23 20:36:52 +08:00
self.timebar.connect('seeked', lambda a,v: self.mpd.seekcur(v))
2023-08-24 18:21:45 +08:00
layout.put(self.timebar, 24, 272)
2023-08-17 20:51:59 +08:00
# 控制条
2023-08-18 19:04:51 +08:00
self.ctrl_box = CtrlBox()
self.ctrl_box.connect('clicked', self.ctrl_clicked)
2023-08-24 18:21:45 +08:00
self.ctrl_box.connect('volume_changed', lambda box, vol: self.mpd.setvol(vol))
2023-08-17 20:51:59 +08:00
2023-08-21 19:06:22 +08:00
layout.put(self.ctrl_box, 48, 312)
2023-08-17 20:51:59 +08:00
self.add(layout)
2023-08-24 18:21:45 +08:00
def set_default_style(self):
provider = Gtk.CssProvider()
css = f"""
#SonistWindow {{
2023-08-25 18:33:01 +08:00
background-image: url('{image_dict['disk']}');
2023-08-24 18:21:45 +08:00
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;
}}
#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 on_drag_start(self, widget, event):
self.begin_move_drag(
event.button,
int(event.x_root),
int(event.y_root),
event.time)
2023-08-17 20:51:59 +08:00
def get_mpd_stat(self):
try:
2023-08-23 20:36:52 +08:00
self.stat = self.mpd.status()
except:
2023-08-23 20:36:52 +08:00
self.stat = {}
return self.stat
2023-08-17 20:51:59 +08:00
def set_background_image(self, filepath):
2023-08-21 19:06:22 +08:00
if type(filepath) == str:
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath)
else:
pixbuf = filepath
2023-08-17 20:51:59 +08:00
pixbuf = blur_image(pixbuf)
pixbuf.savev(f"/tmp/sonist_album_cache", 'png', [], [])
css = f"""
#SonistWindow {{
background-image: url('/tmp/sonist_album_cache');
2023-08-22 20:43:40 +08:00
}}
2023-08-17 20:51:59 +08:00
"""
# 加载CSS样式
2023-08-24 18:21:45 +08:00
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)
2023-08-17 00:06:21 +08:00
2023-08-18 19:04:51 +08:00
def ctrl_clicked(self, box, btn):
if btn == 'play_btn':
self.toggle_play()
elif btn == '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)
2023-08-18 19:04:51 +08:00
elif btn == 'prev_btn':
self.prev_song()
2023-08-18 19:04:51 +08:00
elif btn == 'next_btn':
self.next_song()
2023-08-18 19:04:51 +08:00
elif btn == 'vol_btn':
self.toggle_play()
2023-08-18 19:04:51 +08:00
def toggle_play(self):
state = self.stat.get('state')
is_play = state == 'play'
try:
if state == 'stop':
2023-08-23 20:36:52 +08:00
self.mpd.play()
state = 'play'
else:
2023-08-23 20:36:52 +08:00
self.mpd.pause()
state = 'pause' if is_play else 'play'
except:
2023-08-23 20:36:52 +08:00
pass
self.stat['state'] = state
self.update_play_stat(state == 'play')
2023-08-18 19:04:51 +08:00
2023-08-21 19:06:22 +08:00
def prev_song(self):
try:
2023-08-23 20:36:52 +08:00
self.mpd.previous()
except:
2023-08-23 20:36:52 +08:00
pass
2023-08-18 19:04:51 +08:00
def next_song(self):
try:
2023-08-23 20:36:52 +08:00
self.mpd.next()
except:
2023-08-23 20:36:52 +08:00
pass
2023-08-18 19:04:51 +08:00
@idle
def update_play_stat(self, is_play = True):
2023-08-21 19:06:22 +08:00
2023-08-28 10:53:11 +08:00
self.disk.update_state(is_play)
2023-08-18 19:04:51 +08:00
# 切换播放按钮状态
self.ctrl_box.toggle_play_btn(is_play)
2023-08-18 19:04:51 +08:00
2023-08-26 22:28:23 +08:00
@idle
2023-08-23 20:36:52 +08:00
def update_playtime(self, stat = {}):
self.stat = stat
2023-08-26 22:28:23 +08:00
times = stat.get('time') or '0:0'
times = times.split(':')
self.timebar.update_time(int(times[0]), int(times[1]))
2023-08-24 18:21:45 +08:00
2023-08-26 22:28:23 +08:00
2023-08-24 18:21:45 +08:00
@idle
def update_volume(self, vol = 100):
self.ctrl_box.set_volume(vol)
2023-08-26 22:28:23 +08:00
@idle
2023-08-23 20:36:52 +08:00
def sync_state(self, stat = None, song = None, first = False):
self.ctrl_box.disabled = False
2023-08-21 19:06:22 +08:00
2023-08-23 20:36:52 +08:00
self.stat = stat or self.get_mpd_stat()
state = self.stat.get('state')
2023-08-18 19:04:51 +08:00
2023-08-26 22:28:23 +08:00
# 首次启动时, 更新数据库
2023-08-18 19:04:51 +08:00
if first:
self.mpd.update()
song_num = int(self.mpd.stats().get('songs') or 0)
playlist = [it['file'] for it in self.mpd.playlistinfo()]
if song_num == 0:
self.ctrl_box.disabled = True
return
# 这里只做添加, 不做删除, 重建播放列表在设置里
if song_num > 0 and len(playlist) < song_num:
songs = self.mpd.listall()
for it in songs:
if it['file'] in playlist:
continue
self.mpd.add(it['file'])
self.get_mpd_stat()
2023-08-26 22:28:23 +08:00
self.update_play_stat(state == 'play')
self.update_volume(int(self.stat.get('volume') or 100))
2023-08-18 19:04:51 +08:00
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')
2023-08-23 20:36:52 +08:00
song = song or self.mpd.currentsong()
2023-08-26 22:28:23 +08:00
if song.get('id'):
# 更新歌曲信息
self.title_box.update(f"{song.get('artist')} - {song.get('title')}")
2023-08-26 22:28:23 +08:00
title_hex = base64.b64encode(song.get('title').encode()).hex()
2023-08-24 17:04:41 +08:00
filepath = f"{self.app.album_cache_dir}/{title_hex}.png"
songpath = f"{self.app.music_dir}/{song.get('file')}"
2023-08-18 19:04:51 +08:00
if os.path.isfile(filepath):
self.update_album(filepath)
else:
2023-08-22 20:43:40 +08:00
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
2023-08-24 18:21:45 +08:00
@idle
2023-08-23 20:36:52 +08:00
def reset_player(self):
self.ctrl_box.disabled = True
2023-08-24 17:04:41 +08:00
self.title_box.update('mpd is offline...')
2023-08-23 20:36:52 +08:00
self.timebar.update_time()
2023-08-24 17:04:41 +08:00
self.update_album()
2023-08-18 19:04:51 +08:00
@idle
2023-08-25 17:44:50 +08:00
def update_album(self, filepath = None):
2023-08-25 18:33:01 +08:00
self.set_background_image(filepath or image_dict['disk'])
2023-08-28 10:53:11 +08:00
self.disk.update_album(filepath)
2023-08-18 19:04:51 +08:00