diff --git a/main.py b/main.py index 5bf1421..042bd07 100755 --- a/main.py +++ b/main.py @@ -8,38 +8,18 @@ from pprint import pprint as print 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 about_app import AboutWindow - from mpd import MPDClient app_id = 'fun.wkit.sonist' 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(): with open(f'{home_dir}/.mpd/mpd.conf', 'r') as f: @@ -60,8 +40,13 @@ class Application(Gtk.Application): self.timer = None 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() + 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) @@ -82,7 +67,6 @@ class Application(Gtk.Application): self.about = AboutWindow() self.add_window(self.window) self.window.show_all() - # self.about.show_all() self.connect_mpd() @@ -95,6 +79,8 @@ class Application(Gtk.Application): print('朕要休息了~~~') + def quit_all(self): + self.remove_window(self.window) """ class ApplicationService(dbus.service.Object): def __init__(self, app): diff --git a/ui/ctrl_box.py b/ui/ctrl_box.py index 12be310..8b54866 100644 --- a/ui/ctrl_box.py +++ b/ui/ctrl_box.py @@ -64,7 +64,7 @@ class CtrlBox(Gtk.Box): self.emit('clicked', 'next_btn') elif btn == self.vol_btn: - self.emit('clicked','vol_btn') + self.emit('clicked', 'vol_btn') def toggle_play_btn(self, on = True): diff --git a/ui/option_menu.py b/ui/option_menu.py index 3a23c1e..b39e860 100644 --- a/ui/option_menu.py +++ b/ui/option_menu.py @@ -7,6 +7,10 @@ from gi.repository import Gtk, Gdk, GObject from .image import ScaleImage class OptionMenu(Gtk.Menu): + + app = None + on_top = False + def __init__(self, app): Gtk.Menu.__init__(self) self.app = app @@ -46,13 +50,20 @@ class OptionMenu(Gtk.Menu): match(item.name): case '首选项': pass + 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 '退出应用': - pass + self.app.quit_all() + case '关于播放器': self.app.about.present() - pass diff --git a/ui/text.py b/ui/text.py deleted file mode 100644 index 9e7277f..0000000 --- a/ui/text.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ui/timebar.py b/ui/timebar.py index a1f1070..3bc2a1e 100644 --- a/ui/timebar.py +++ b/ui/timebar.py @@ -36,9 +36,11 @@ class Timebar(Gtk.Fixed): self.duration = Gtk.Label() 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_xalign(1) - self.duration.set_name('text') self.slider.connect('change-value', self.debounce) self.slider.set_sensitive(False) diff --git a/ui/title_text.py b/ui/title_text.py new file mode 100644 index 0000000..b21f988 --- /dev/null +++ b/ui/title_text.py @@ -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 \ No newline at end of file diff --git a/utils.py b/utils.py index db45f6f..d889a88 100644 --- a/utils.py +++ b/utils.py @@ -1,10 +1,14 @@ #!/usr/bin/env python3 -import gi, io, base64 -from gi.repository import Gdk, GLib, GdkPixbuf, Gio +import gi, io, base64, threading +from gi.repository import Gdk, GLib, GdkPixbuf, Gio, GObject from PIL import Image, ImageFilter + +empty_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 1, 1) + + def pixbuf_to_pil(pixbuf): data = pixbuf.get_pixels() w = pixbuf.get_width() @@ -23,18 +27,28 @@ def pil_to_pixbuf(img): alpha = img.mode == 'RGBA' w, h = img.size 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): data = pic.data 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): data = base64.b64decode(base64_str) 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): # 加载图片并确认该图片为RGBA模式,保证透明度 @@ -43,4 +57,34 @@ def blur_image(pixbuf): img = img.filter(ImageFilter.GaussianBlur(radius = 32)) img.alpha_composite(mask) - return pil_to_pixbuf(img) \ No newline at end of file + 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 + diff --git a/window.py b/window.py index c94b24d..bf8c5b8 100644 --- a/window.py +++ b/window.py @@ -1,28 +1,22 @@ #!/usr/bin/env python3 -import gi, sys, os, mutagen, threading -# from pprint import pprint as print +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 +from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf, idle from ui.image import ScaleImage from ui.slider import Slider 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.timebar import Timebar from ui.option_menu import OptionMenu -# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时) -def idle(func): - def wrapper(*args): - GObject.idle_add(func, *args) - return wrapper + class SonistWindow(Gtk.Window): @@ -36,7 +30,7 @@ class SonistWindow(Gtk.Window): self.app = app 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('online', lambda o: self.sync_state(None, None, True)) @@ -88,10 +82,7 @@ class SonistWindow(Gtk.Window): # title - # self.title_box = TextBox(256, 20) - self.title_box = Gtk.Label() - self.title_box.set_name('text') - self.title_box.set_text('mpd loading...') + self.title_box = TitleText() layout.put(self.title_box, 27, 244) @@ -99,7 +90,7 @@ class SonistWindow(Gtk.Window): # 播放进度 self.timebar = Timebar() 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': 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) - filepath = f"./album/{song['title']}.png" - songpath = f"{self.app.music_dir}/{song['file']}" + 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 not None: + if pic is None: + self.update_album() + else: album = pic_to_pixbuf(pic) self.update_album(album) - except: + + except Exception as err: pass @idle def reset_player(self): 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.update_album('./usr/share/sonist/avatar.jpg') + self.update_album() @idle - def update_album(self, filepath): + def update_album(self, filepath = './usr/share/sonist/avatar.jpg'): self.set_background_image(filepath) self.album.reset(filepath).set_radius(64) - def quited(self, win): - self.app.remove_window(self) -