优化UI组件
parent
942f16cdeb
commit
1a4cb930d1
34
main.py
34
main.py
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
21
ui/text.py
21
ui/text.py
|
@ -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
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
48
utils.py
48
utils.py
|
@ -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
|
||||||
|
try:
|
||||||
return GdkPixbuf.Pixbuf.new_from_bytes(GLib.Bytes.new(data), GdkPixbuf.Colorspace.RGB, alpha, 8, w, h, rowstride)
|
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)
|
||||||
|
try:
|
||||||
return GdkPixbuf.Pixbuf.new_from_stream(input_stream, None)
|
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)
|
||||||
|
try:
|
||||||
return GdkPixbuf.Pixbuf.new_from_stream(input_stream, None)
|
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
|
||||||
|
|
||||||
|
|
50
window.py
50
window.py
|
@ -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)
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue