This commit is contained in:
Andrew Dunai 2018-01-02 11:26:55 +02:00
parent 77e3f44fa5
commit 5b88ae681a
7 changed files with 160 additions and 51 deletions

31
.flake8 Normal file
View file

@ -0,0 +1,31 @@
[flake8]
# ignore = D203
max-line-length=120
ignore =
# blank line at end of file
W391
exclude =
# Generated stuff
gen,
# Coverage HTML report
htmlcov,
# Logs
log,
# Requirements
requirements,
# Temporary stuff
tmp,
# Virtual envs
.env,
.env2,
.env3,
.env36,
.venv,
.git,
wsgi.py,
utils,
# VLC bindings
vlc.py

View file

@ -1,2 +0,0 @@
from clay.app import main

View file

@ -1,23 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# pylint: disable=wrong-import-position
# pylint: disable=too-many-instance-attributes
"""
Main app entrypoint.
"""
import sys import sys
sys.path.append('.') sys.path.append('.') # noqa
import urwid import urwid
from clay.player import player from clay.player import player
from clay.widgets import PlayProgress from clay.widgets import PlayProgress
from clay.pages import StartUp, Error from clay.pages import StartUp
from clay.mylibrary import MyLibrary from clay.mylibrary import MyLibrary
from clay.myplaylists import MyPlaylists from clay.myplaylists import MyPlaylists
from clay.playerqueue import Queue from clay.playerqueue import Queue
from clay.settings import Settings from clay.settings import Settings
from clay.notifications import NotificationArea
# from clay import hotkeys # from clay import hotkeys
loop = None
PALETTE = [ PALETTE = [
('logo', '', '', '', '#F54', ''), ('logo', '', '', '', '#F54', ''),
@ -45,24 +48,39 @@ PALETTE = [
('flag', '', '', '', '#AAA', ''), ('flag', '', '', '', '#AAA', ''),
('flag-active', '', '', '', '#F54', ''), ('flag-active', '', '', '', '#F54', ''),
('notification', '', '', '', '#F54', '#222'),
] ]
class AppWidget(urwid.Frame): class AppWidget(urwid.Frame):
"""
Root widget.
Handles tab switches, global keypresses etc.
"""
class Tab(urwid.Text): class Tab(urwid.Text):
"""
Represents a single tab in header tabbar.
"""
def __init__(self, page): def __init__(self, page):
self.page = page self.page = page
# self.attrwrap = urwid.AttrWrap(urwid.Text(), 'panel')
super(AppWidget.Tab, self).__init__( super(AppWidget.Tab, self).__init__(
self.get_title() self.get_title()
) )
def set_active(self, active): def set_active(self, active):
"""
Mark tab visually as active.
"""
self.set_text( self.set_text(
[('panel_focus' if active else 'panel', self.get_title())] [('panel_focus' if active else 'panel', self.get_title())]
) )
def get_title(self): def get_title(self):
"""
Render tab title.
"""
return ' {} {} '.format( return ' {} {} '.format(
self.page.key, self.page.key,
self.page.name self.page.name
@ -71,7 +89,6 @@ class AppWidget(urwid.Frame):
def __init__(self): def __init__(self):
self.pages = [ self.pages = [
StartUp(self), StartUp(self),
Error(self),
MyLibrary(self), MyLibrary(self),
MyPlaylists(self), MyPlaylists(self),
Queue(self), Queue(self),
@ -83,6 +100,7 @@ class AppWidget(urwid.Frame):
in self.pages in self.pages
if hasattr(page, 'key') if hasattr(page, 'key')
] ]
NotificationArea.set_app(self)
self.header = urwid.Pile([ self.header = urwid.Pile([
# urwid.Divider('\u2500'), # urwid.Divider('\u2500'),
urwid.AttrWrap(urwid.Columns([ urwid.AttrWrap(urwid.Columns([
@ -90,6 +108,7 @@ class AppWidget(urwid.Frame):
for tab for tab
in self.tabs in self.tabs
], dividechars=1), 'panel'), ], dividechars=1), 'panel'),
NotificationArea.get()
# urwid.Divider('\u2500') # urwid.Divider('\u2500')
]) ])
self.seekbar = PlayProgress( self.seekbar = PlayProgress(
@ -116,27 +135,48 @@ class AppWidget(urwid.Frame):
body=self.current_page body=self.current_page
) )
self.loop = None
player.media_position_changed += self.media_position_changed player.media_position_changed += self.media_position_changed
player.media_state_changed += self.media_state_changed player.media_state_changed += self.media_state_changed
player.track_changed += self.track_changed player.track_changed += self.track_changed
player.playback_flags_changed += self.playback_flags_changed player.playback_flags_changed += self.playback_flags_changed
def set_loop(self, loop):
"""
Assign a MainLoop to this app.
"""
self.loop = loop
def media_position_changed(self, progress): def media_position_changed(self, progress):
"""
Update slider in seekbar.
Called when current play position changes.
"""
if progress < 0: if progress < 0:
progress = 0 progress = 0
self.seekbar.set_completion(progress * 100) self.seekbar.set_completion(progress * 100)
loop.draw_screen() self.loop.draw_screen()
# sleep(0.2)
# self.set_page(MyLibrary()) def media_state_changed(self, _):
"""
def media_state_changed(self, is_playing): Update seekbar.
Called when playback is paused/unpaused.
"""
self.seekbar.update() self.seekbar.update()
def track_changed(self, track): def track_changed(self, track):
"""
Update displayed track in seekbar.
Called when current track changes.
"""
self.seekbar.set_track(track) self.seekbar.set_track(track)
def playback_flags_changed(self): def playback_flags_changed(self):
"""
Update seekbar flags.
Called when random/repeat flags change.
"""
self.shuffle_el.attr = 'flag-active' \ self.shuffle_el.attr = 'flag-active' \
if player.get_is_random() \ if player.get_is_random() \
else 'flag' else 'flag'
@ -145,15 +185,10 @@ class AppWidget(urwid.Frame):
else 'flag' else 'flag'
self.seekbar.update() self.seekbar.update()
def set_page(self, classname, *args): def set_page(self, classname):
# if isinstance(page_class, str): """
# page_class = [ Switch to a different tab.
# page """
# for page
# in self.pages
# if page.__name__ == page_class
# ][0]
# self.current_page = page_class(self, *args)
page = [page for page in self.pages if page.__class__.__name__ == classname][0] page = [page for page in self.pages if page.__class__.__name__ == classname][0]
self.current_page = page self.current_page = page
self.contents['body'] = (page, None) self.contents['body'] = (page, None)
@ -169,12 +204,18 @@ class AppWidget(urwid.Frame):
page.start() page.start()
def redraw(self): def redraw(self):
if loop: """
loop.draw_screen() Redraw screen.
Needs to be called by other widgets if UI was changed from a different thread.
"""
if self.loop:
self.loop.draw_screen()
def keypress(self, size, key): def keypress(self, size, key):
if isinstance(self.current_page, StartUp): """
return Handle keypress.
Can switch tabs, control playbackm, flags, notifications and app state.
"""
for tab in self.tabs: for tab in self.tabs:
if 'meta {}'.format(tab.page.key) == key: if 'meta {}'.format(tab.page.key) == key:
self.set_page(tab.page.__class__.__name__) self.set_page(tab.page.__class__.__name__)
@ -196,18 +237,22 @@ class AppWidget(urwid.Frame):
player.set_repeat_one(not player.get_is_repeat_one()) player.set_repeat_one(not player.get_is_repeat_one())
elif key == 'ctrl q': elif key == 'ctrl q':
sys.exit(0) sys.exit(0)
elif key == 'esc':
NotificationArea.close_all()
else: else:
super(AppWidget, self).keypress(size, key) super(AppWidget, self).keypress(size, key)
def main(): def main():
global loop """
Application entrypoint.
"""
app_widget = AppWidget() app_widget = AppWidget()
loop = urwid.MainLoop(app_widget, PALETTE) loop = urwid.MainLoop(app_widget, PALETTE)
app_widget.set_loop(loop)
loop.screen.set_terminal_properties(256) loop.screen.set_terminal_properties(256)
loop.run() loop.run()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -15,7 +15,6 @@ def async(fn):
result = fn(*args, **kwargs) result = fn(*args, **kwargs)
except Exception as e: except Exception as e:
callback(None, e, **extra) callback(None, e, **extra)
raise
else: else:
callback(result, None, **extra) callback(result, None, **extra)
@ -147,5 +146,6 @@ class GP(object):
def is_authenticated(self): def is_authenticated(self):
return self.mc.is_authenticated() return self.mc.is_authenticated()
gp = GP() gp = GP()

50
clay/notifications.py Normal file
View file

@ -0,0 +1,50 @@
import urwid
class NotificationArea(urwid.Pile):
instance = None
app = None
TEMPLATE = ' \u26A1 {}'
def __init__(self):
super().__init__([])
@classmethod
def get(cls):
if cls.instance is None:
cls.instance = NotificationArea()
return cls.instance
@classmethod
def set_app(cls, app):
cls.app = app
@classmethod
def notify(cls, message):
cls.instance._notify(message)
@classmethod
def close_all(cls):
cls.instance._close_all()
def _notify(self, message):
text = urwid.Text(self.__class__.TEMPLATE.format(message))
self.contents.append(
(
urwid.AttrWrap(
urwid.Columns([
text,
('pack', urwid.Text('[Hit ESC to close] '))
]),
'notification'
),
('weight', 1)
)
)
self.__class__.app.redraw()
def _close_all(self):
self.contents[:] = []

View file

@ -2,6 +2,7 @@ import urwid
from clay.settings import Settings from clay.settings import Settings
from clay.gp import gp from clay.gp import gp
from clay.meta import VERSION from clay.meta import VERSION
from clay.notifications import NotificationArea
class StartUp(urwid.Filler): class StartUp(urwid.Filler):
@ -29,14 +30,11 @@ class StartUp(urwid.Filler):
def on_login(self, success, error): def on_login(self, success, error):
if error: if error:
self.app.set_page( NotificationArea.notify('Failed to log in: {}'.format(str(error)))
'Error',
'Failed to log in: {}'.format(str(error))
)
return return
if not success: if not success:
self.app.set_page( NotificationArea.notify(
'Error',
'Google Play Music login failed ' 'Google Play Music login failed '
'(API returned false)' '(API returned false)'
) )
@ -54,20 +52,7 @@ class StartUp(urwid.Filler):
callback=self.on_login callback=self.on_login
) )
else: else:
self.app.set_page( NotificationArea.notify(
'Error',
'Please set your credentials on the settings page.' 'Please set your credentials on the settings page.'
) )
class Error(urwid.Filler):
def __init__(self, app):
self.text = urwid.Text('Error'),
super(Error, self).__init__(
self.text,
valign='top'
)
def set_error(self, error):
self.text = 'Error:\n\n{}'.format(str(error))

View file

@ -12,7 +12,7 @@ setup(
packages=find_packages(), packages=find_packages(),
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'clay=clay.__main__:main' 'clay=clay.app:main'
] ]
} }
) )