优化UI组件

master
yutent 2023-08-24 17:04:41 +08:00
parent 942f16cdeb
commit 1a4cb930d1
8 changed files with 122 additions and 84 deletions

34
main.py
View File

@ -8,38 +8,18 @@ from pprint import pprint as print
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject from gi.repository import Gtk
from utils import run_async
from window import SonistWindow from window import SonistWindow
from about_app import AboutWindow from about_app import AboutWindow
from mpd import MPDClient from mpd import MPDClient
app_id = 'fun.wkit.sonist' app_id = 'fun.wkit.sonist'
home_dir = os.getenv('HOME') home_dir = os.getenv('HOME')
# 定义一个异步修饰器, 用于在子线程中运行一些会阻塞主线程的任务
def run_async(func):
def wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.daemon = True
thread.start()
return thread
return wrapper
# 类型js的settimeout的修饰器
def set_timeout(timeout = 0.5):
def decorator(callback):
def wrapper(*args):
t = threading.Timer(timeout, callback, args=args)
t.start()
return t
return wrapper
return decorator
def get_music_dir(): def get_music_dir():
with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f: with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f:
@ -60,8 +40,13 @@ class Application(Gtk.Application):
self.timer = None self.timer = None
self.music_dir = get_music_dir() self.music_dir = get_music_dir()
self.album_cache_dir = f"{home_dir}/.cache/sonist/album"
self.lyric_cache_dir = f"{home_dir}/.cache/sonist/lyric"
self.mpd = MPDClient() self.mpd = MPDClient()
os.makedirs(self.album_cache_dir, exist_ok = True)
os.makedirs(self.lyric_cache_dir, exist_ok = True)
self.connect('window-removed', self.on_window_removed) self.connect('window-removed', self.on_window_removed)
@ -82,7 +67,6 @@ class Application(Gtk.Application):
self.about = AboutWindow() self.about = AboutWindow()
self.add_window(self.window) self.add_window(self.window)
self.window.show_all() self.window.show_all()
# self.about.show_all()
self.connect_mpd() self.connect_mpd()
@ -95,6 +79,8 @@ class Application(Gtk.Application):
print('朕要休息了~~~') print('朕要休息了~~~')
def quit_all(self):
self.remove_window(self.window)
""" class ApplicationService(dbus.service.Object): """ class ApplicationService(dbus.service.Object):
def __init__(self, app): def __init__(self, app):

View File

@ -64,7 +64,7 @@ class CtrlBox(Gtk.Box):
self.emit('clicked', 'next_btn') self.emit('clicked', 'next_btn')
elif btn == self.vol_btn: elif btn == self.vol_btn:
self.emit('clicked','vol_btn') self.emit('clicked', 'vol_btn')
def toggle_play_btn(self, on = True): def toggle_play_btn(self, on = True):

View File

@ -7,6 +7,10 @@ from gi.repository import Gtk, Gdk, GObject
from .image import ScaleImage from .image import ScaleImage
class OptionMenu(Gtk.Menu): class OptionMenu(Gtk.Menu):
app = None
on_top = False
def __init__(self, app): def __init__(self, app):
Gtk.Menu.__init__(self) Gtk.Menu.__init__(self)
self.app = app self.app = app
@ -46,13 +50,20 @@ class OptionMenu(Gtk.Menu):
match(item.name): match(item.name):
case '首选项': case '首选项':
pass pass
case '窗口置顶': case '窗口置顶':
pass self.on_top = not self.on_top
self.app.window.set_keep_above(self.on_top)
if self.on_top:
item.select()
else:
item.deselect()
case '退出应用': case '退出应用':
pass self.app.quit_all()
case '关于播放器': case '关于播放器':
self.app.about.present() self.app.about.present()
pass

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class TextBox(Gtk.Box):
def __init__(self, width, height):
Gtk.Box.__init__(self)
self.set_size_request(width, height)
self.label = Gtk.Label()
self.add(self.label)
def set_text(self, string):
self.label.set_text('孙晓 - 丹歌惊鸿')
return self

View File

@ -36,9 +36,11 @@ class Timebar(Gtk.Fixed):
self.duration = Gtk.Label() self.duration = Gtk.Label()
self.curr.set_name('text') self.curr.set_name('text')
self.curr.set_selectable(False)
self.duration.set_name('text')
self.duration.set_selectable(False)
self.duration.set_justify(Gtk.Justification.RIGHT) self.duration.set_justify(Gtk.Justification.RIGHT)
self.duration.set_xalign(1) self.duration.set_xalign(1)
self.duration.set_name('text')
self.slider.connect('change-value', self.debounce) self.slider.connect('change-value', self.debounce)
self.slider.set_sensitive(False) self.slider.set_sensitive(False)

22
ui/title_text.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class TitleText(Gtk.Label):
def __init__(self, text = 'mpd loading...'):
Gtk.Box.__init__(self)
self.set_size_request(256, 20)
self.set_name('text')
self.set_hexpand(True)
self.set_hexpand_set(True)
self.set_halign(Gtk.Align.CENTER)
self.set_text(text)
def update(self, text):
self.set_text(text)
return self

View File

@ -1,10 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import gi, io, base64 import gi, io, base64, threading
from gi.repository import Gdk, GLib, GdkPixbuf, Gio from gi.repository import Gdk, GLib, GdkPixbuf, Gio, GObject
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
empty_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 1, 1)
def pixbuf_to_pil(pixbuf): def pixbuf_to_pil(pixbuf):
data = pixbuf.get_pixels() data = pixbuf.get_pixels()
w = pixbuf.get_width() w = pixbuf.get_width()
@ -23,18 +27,28 @@ def pil_to_pixbuf(img):
alpha = img.mode == 'RGBA' alpha = img.mode == 'RGBA'
w, h = img.size w, h = img.size
rowstride = w * 4 rowstride = w * 4
return GdkPixbuf.Pixbuf.new_from_bytes(GLib.Bytes.new(data), GdkPixbuf.Colorspace.RGB, alpha, 8, w, h, rowstride) try:
return GdkPixbuf.Pixbuf.new_from_bytes(GLib.Bytes.new(data), GdkPixbuf.Colorspace.RGB, alpha, 8, w, h, rowstride)
except:
return empty_pixbuf
def pic_to_pixbuf(pic): def pic_to_pixbuf(pic):
data = pic.data data = pic.data
input_stream = Gio.MemoryInputStream.new_from_data(data, None) input_stream = Gio.MemoryInputStream.new_from_data(data, None)
return GdkPixbuf.Pixbuf.new_from_stream(input_stream, None) try:
return GdkPixbuf.Pixbuf.new_from_stream(input_stream, None)
except:
return empty_pixbuf
def base64_to_pixbuf(base64_str): def base64_to_pixbuf(base64_str):
data = base64.b64decode(base64_str) data = base64.b64decode(base64_str)
input_stream = Gio.MemoryInputStream.new_from_data(data, None) input_stream = Gio.MemoryInputStream.new_from_data(data, None)
return GdkPixbuf.Pixbuf.new_from_stream(input_stream, None) try:
return GdkPixbuf.Pixbuf.new_from_stream(input_stream, None)
except:
return empty_pixbuf
def blur_image(pixbuf): def blur_image(pixbuf):
# 加载图片并确认该图片为RGBA模式保证透明度 # 加载图片并确认该图片为RGBA模式保证透明度
@ -44,3 +58,33 @@ def blur_image(pixbuf):
img.alpha_composite(mask) img.alpha_composite(mask)
return pil_to_pixbuf(img) return pil_to_pixbuf(img)
# 定义一个异步修饰器, 用于在子线程中运行一些会阻塞主线程的任务
def run_async(func):
def wrapper(*args, **kwargs):
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
thread.daemon = True
thread.start()
return thread
return wrapper
# 类型js的settimeout的修饰器
def set_timeout(timeout = 0.5):
def decorator(callback):
def wrapper(*args):
t = threading.Timer(timeout, callback, args=args)
t.start()
return t
return wrapper
return decorator
# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时)
def idle(func):
def wrapper(*args):
GObject.idle_add(func, *args)
return wrapper

View File

@ -1,28 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import gi, sys, os, mutagen, threading import gi, sys, os, mutagen, base64
# from pprint import pprint as print
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject
from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf, idle
from ui.image import ScaleImage from ui.image import ScaleImage
from ui.slider import Slider from ui.slider import Slider
from ui.image_button import ImageButton from ui.image_button import ImageButton
from ui.text import TextBox from ui.title_text import TitleText
from ui.ctrl_box import CtrlBox from ui.ctrl_box import CtrlBox
from ui.timebar import Timebar from ui.timebar import Timebar
from ui.option_menu import OptionMenu from ui.option_menu import OptionMenu
# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时)
def idle(func):
def wrapper(*args):
GObject.idle_add(func, *args)
return wrapper
class SonistWindow(Gtk.Window): class SonistWindow(Gtk.Window):
@ -36,7 +30,7 @@ class SonistWindow(Gtk.Window):
self.app = app self.app = app
self.mpd = app.mpd self.mpd = app.mpd
self.connect("destroy", self.quited) self.connect("destroy", lambda win: app.remove_window(win))
self.mpd.connect('offline', lambda o: self.reset_player()) self.mpd.connect('offline', lambda o: self.reset_player())
self.mpd.connect('online', lambda o: self.sync_state(None, None, True)) self.mpd.connect('online', lambda o: self.sync_state(None, None, True))
@ -88,10 +82,7 @@ class SonistWindow(Gtk.Window):
# title # title
# self.title_box = TextBox(256, 20) self.title_box = TitleText()
self.title_box = Gtk.Label()
self.title_box.set_name('text')
self.title_box.set_text('mpd loading...')
layout.put(self.title_box, 27, 244) layout.put(self.title_box, 27, 244)
@ -99,7 +90,7 @@ class SonistWindow(Gtk.Window):
# 播放进度 # 播放进度
self.timebar = Timebar() self.timebar = Timebar()
self.timebar.connect('seeked', lambda a,v: self.mpd.seekcur(v)) self.timebar.connect('seeked', lambda a,v: self.mpd.seekcur(v))
layout.put(self.timebar, 24, 270) layout.put(self.timebar, 24, 276)
# 控制条 # 控制条
@ -245,43 +236,46 @@ class SonistWindow(Gtk.Window):
if played != 'stop': if played != 'stop':
song = song or self.mpd.currentsong() song = song or self.mpd.currentsong()
# 更新歌曲信息 # 更新歌曲信息
self.title_box.set_text(f"{song.get('artist')} - {song.get('title')}") self.title_box.update(f"{song.get('artist')} - {song.get('title')}")
self.update_playtime(self.stat) self.update_playtime(self.stat)
filepath = f"./album/{song['title']}.png" title = song['file']
songpath = f"{self.app.music_dir}/{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): if os.path.isfile(filepath):
self.update_album(filepath) self.update_album(filepath)
else: else:
id3 = mutagen.File(songpath) id3 = mutagen.File(songpath)
pic = None
try: try:
if id3.tags.get('APIC:'): if id3.tags.get('APIC:'):
pic = id3.tags['APIC:'] pic = id3.tags['APIC:']
elif len(id3.pictures) > 0: elif len(id3.pictures) > 0:
pic = id3.pictures[0] pic = id3.pictures[0]
if pic is not None: if pic is None:
self.update_album()
else:
album = pic_to_pixbuf(pic) album = pic_to_pixbuf(pic)
self.update_album(album) self.update_album(album)
except:
except Exception as err:
pass pass
@idle @idle
def reset_player(self): def reset_player(self):
self.ctrl_box.disabled = True self.ctrl_box.disabled = True
self.title_box.set_text('mpd is offline...') self.title_box.update('mpd is offline...')
self.timebar.update_time() self.timebar.update_time()
self.update_album('./usr/share/sonist/avatar.jpg') self.update_album()
@idle @idle
def update_album(self, filepath): def update_album(self, filepath = './usr/share/sonist/avatar.jpg'):
self.set_background_image(filepath) self.set_background_image(filepath)
self.album.reset(filepath).set_radius(64) self.album.reset(filepath).set_radius(64)
def quited(self, win):
self.app.remove_window(self)