From 059ed7a5f0589ee14d8d4d654d3eb269e65cfd1e Mon Sep 17 00:00:00 2001 From: croneter Date: Sat, 6 Apr 2019 10:26:01 +0200 Subject: [PATCH] Switch to stream playback, part II --- resources/lib/itemtypes/movies.py | 11 +- resources/lib/kodi_db/video.py | 4 +- resources/lib/kodimonitor.py | 17 +++ resources/lib/playstrm.py | 138 ++++++++++-------- resources/lib/webservice.py | 32 ++-- resources/lib/widgets.py | 2 +- resources/lib/windows/resume.py | 81 ++++++++++ .../default/1080i/script-plex-resume.xml | 112 ++++++++++++++ .../default/media/dialogs/dialog_back.png | Bin 0 -> 15141 bytes .../skins/default/media/dialogs/menu_back.png | Bin 0 -> 15358 bytes .../default/media/dialogs/menu_bottom.png | Bin 0 -> 14781 bytes .../skins/default/media/dialogs/menu_top.png | Bin 0 -> 14768 bytes .../skins/default/media/dialogs/white.jpg | Bin 0 -> 8060 bytes 13 files changed, 311 insertions(+), 86 deletions(-) create mode 100644 resources/lib/windows/resume.py create mode 100644 resources/skins/default/1080i/script-plex-resume.xml create mode 100644 resources/skins/default/media/dialogs/dialog_back.png create mode 100644 resources/skins/default/media/dialogs/menu_back.png create mode 100644 resources/skins/default/media/dialogs/menu_bottom.png create mode 100644 resources/skins/default/media/dialogs/menu_top.png create mode 100644 resources/skins/default/media/dialogs/white.jpg diff --git a/resources/lib/itemtypes/movies.py b/resources/lib/itemtypes/movies.py index 409534d0..203f4ef8 100644 --- a/resources/lib/itemtypes/movies.py +++ b/resources/lib/itemtypes/movies.py @@ -72,10 +72,13 @@ class Movie(ItemBase): scraper='metadata.local') if do_indirect: # Set plugin path and media flags using real filename - filename = api.file_name(force_first_media=True) - path = 'plugin://%s.movies/' % v.ADDON_ID - filename = ('%s?plex_id=%s&plex_type=%s&mode=play&filename=%s' - % (path, plex_id, v.PLEX_TYPE_MOVIE, filename)) + path = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT + filename = '{0}/file.strm?kodi_id={1}&kodi_type={2}&plex_id={0}&plex_type={3}&name={4}' + filename = filename.format(plex_id, + kodi_id, + v.KODI_TYPE_MOVIE, + v.PLEX_TYPE_MOVIE, + api.file_name(force_first_media=True)) playurl = filename kodi_pathid = self.kodidb.get_path(path) diff --git a/resources/lib/kodi_db/video.py b/resources/lib/kodi_db/video.py index 98fd92fb..1e959938 100644 --- a/resources/lib/kodi_db/video.py +++ b/resources/lib/kodi_db/video.py @@ -5,11 +5,11 @@ from logging import getLogger from sqlite3 import IntegrityError from . import common -from .. import path_ops, timing, variables as v, app +from .. import path_ops, timing, variables as v LOG = getLogger('PLEX.kodi_db.video') -MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID +MOVIE_PATH = 'http://127.0.0.1:%s/plex/kodi/movies/' % v.WEBSERVICE_PORT SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID diff --git a/resources/lib/kodimonitor.py b/resources/lib/kodimonitor.py index f5ea2288..f35db30b 100644 --- a/resources/lib/kodimonitor.py +++ b/resources/lib/kodimonitor.py @@ -449,6 +449,23 @@ def _playback_cleanup(ended=False): app.PLAYSTATE.active_players = set() LOG.info('Finished PKC playback cleanup') + def Playlist_OnAdd(self, server, data, *args, **kwargs): + ''' + Detect widget playback. Widget for some reason, use audio playlists. + ''' + LOG.debug('Playlist_OnAdd: %s, %s', server, data) + if data['position'] == 0: + if data['playlistid'] == 0: + utils.window('plex.playlist.audio', value='true') + else: + utils.window('plex.playlist.audio', clear=True) + self.playlistid = data['playlistid'] + if utils.window('plex.playlist.start') and data['position'] == int(utils.window('plex.playlist.start')) + 1: + + LOG.info("--[ playlist ready ]") + utils.window('plex.playlist.ready', value='true') + utils.window('plex.playlist.start', clear=True) + def _record_playstate(status, ended): if not status['plex_id']: diff --git a/resources/lib/playstrm.py b/resources/lib/playstrm.py index cfc76844..08fa690d 100644 --- a/resources/lib/playstrm.py +++ b/resources/lib/playstrm.py @@ -3,11 +3,12 @@ from __future__ import absolute_import, division, unicode_literals from logging import getLogger import xbmc -import xbmcgui from .plex_api import API -from . import plex_function as PF, utils, json_rpc, variables as v, \ - widgets +from .playutils import PlayUtils +from .windows.resume import resume_dialog +from . import app, plex_functions as PF, utils, json_rpc, variables as v, \ + widgets, playlist_func as PL, playqueue as PQ LOG = getLogger('PLEX.playstrm') @@ -51,11 +52,13 @@ class PlayStrm(object): self.transcode = params.get('transcode') if self.transcode is None: self.transcode = utils.settings('playFromTranscode.bool') if utils.settings('playFromStream.bool') else None - if utils.window('plex.playlist.audio.bool'): - LOG.info('Audio playlist detected') - self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_MUSIC) + if utils.window('plex.playlist.audio'): + LOG.debug('Audio playlist detected') + self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_AUDIO) else: - self.kodi_playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + LOG.debug('Video playlist detected') + self.playqueue = PQ.get_playqueue_from_type(v.KODI_TYPE_VIDEO) + self.kodi_playlist = self.playqueue.kodi_pl def __repr__(self): return ("{{" @@ -97,6 +100,7 @@ class PlayStrm(object): else: self.xml[0].set('pkc_db_item', None) self.api = API(self.xml[0]) + self.playqueue_item = PL.playlist_item_from_xml(self.xml[0]) def start_playback(self, index=0): LOG.debug('Starting playback at %s', index) @@ -111,7 +115,7 @@ class PlayStrm(object): else: self.start_index = max(self.kodi_playlist.getposition(), 0) self.index = self.start_index - listitem = xbmcgui.ListItem() + listitem = widgets.get_listitem(self.xml[0]) self._set_playlist(listitem) LOG.info('Initiating play for %s', self) if not delayed: @@ -159,24 +163,41 @@ class PlayStrm(object): action set in Kodi for accurate resume behavior. ''' seektime = self._resume() + trailers = False if (not seektime and self.plex_type == v.PLEX_TYPE_MOVIE and utils.settings('enableCinema') == 'true'): - self._set_intros() - - play = playutils.PlayUtilsStrm(self.xml, self.transcode, self.server_id, self.info['Server']) - source = play.select_source(play.get_sources()) - - if not source: - raise PlayStrmException('Playback selection cancelled') - - play.set_external_subs(source, listitem) - self.set_listitem(self.xml, listitem, self.kodi_id, seektime) - listitem.setPath(self.xml['PlaybackInfo']['Path']) - playutils.set_properties(self.xml, self.xml['PlaybackInfo']['Method'], self.server_id) - - self.kodi_playlist.add(url=self.xml['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + if utils.settings('askCinema') == "true": + # "Play trailers?" + trailers = utils.yesno_dialog(utils.lang(29999), + utils.lang(33016)) or False + else: + trailers = True + LOG.debug('Playing trailers: %s', trailers) + xml = PF.init_plex_playqueue(self.plex_id, + self.xml.get('librarySectionUUID'), + mediatype=self.plex_type, + trailers=trailers) + if xml is None: + LOG.error('Could not get playqueue for UUID %s for %s', + self.xml.get('librarySectionUUID'), self) + # "Play error" + utils.dialog('notification', + utils.lang(29999), + utils.lang(30128), + icon='{error}') + app.PLAYSTATE.context_menu_play = False + app.PLAYSTATE.force_transcode = False + app.PLAYSTATE.resume_playback = False + return + PL.get_playlist_details_from_xml(self.playqueue, xml) + # See that we add trailers, if they exist in the xml return + self._set_intros(xml) + listitem.setSubtitles(self.api.cache_external_subs()) + play = PlayUtils(self.api, self.playqueue_item) + url = play.getPlayUrl().encode('utf-8') + listitem.setPath(url) + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) self.index += 1 - if self.xml.get('PartCount'): self._set_additional_parts() @@ -203,52 +224,41 @@ class PlayStrm(object): utils.window('plex.autoplay.bool', value='true') return seektime - def _set_intros(self): + def _set_intros(self, xml): ''' if we have any play them when the movie/show is not being resumed. ''' - if self.info['Intros']['Items']: - enabled = True - - if utils.settings('askCinema') == 'true': - - resp = dialog('yesno', heading='{emby}', line1=_(33016)) - if not resp: - - enabled = False - LOG.info('Skip trailers.') - - if enabled: - for intro in self.info['Intros']['Items']: - - listitem = xbmcgui.ListItem() - LOG.info('[ intro/%s/%s ] %s', intro['plex_id'], self.index, intro['Name']) - - play = playutils.PlayUtilsStrm(intro, False, self.server_id, self.info['Server']) - source = play.select_source(play.get_sources()) - self.set_listitem(intro, listitem, intro=True) - listitem.setPath(intro['PlaybackInfo']['Path']) - playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id) - - self.kodi_playlist.add(url=intro['PlaybackInfo']['Path'], listitem=listitem, index=self.index) - self.index += 1 - - utils.window('plex.skip.%s' % intro['plex_id'], value='true') + if not len(xml) > 1: + LOG.debug('No trailers returned from the PMS') + return + for intro in xml: + if utils.cast(int, xml.get('ratingKey')) == self.plex_id: + # The main item we're looking at - skip! + continue + api = API(intro) + listitem = widgets.get_listitem(intro) + listitem.setSubtitles(api.cache_external_subs()) + playqueue_item = PL.playlist_item_from_xml(intro) + play = PlayUtils(api, playqueue_item) + url = play.getPlayUrl().encode('utf-8') + listitem.setPath(url) + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) + self.index += 1 + utils.window('plex.skip.%s' % api.plex_id(), value='true') def _set_additional_parts(self): ''' Create listitems and add them to the stack of playlist. ''' - for part in self.info['AdditionalParts']['Items']: - - listitem = xbmcgui.ListItem() - LOG.info('[ part/%s/%s ] %s', part['plex_id'], self.index, part['Name']) - - play = playutils.PlayUtilsStrm(part, self.transcode, self.server_id, self.info['Server']) - source = play.select_source(play.get_sources()) - play.set_external_subs(source, listitem) - self.set_listitem(part, listitem) - listitem.setPath(part['PlaybackInfo']['Path']) - playutils.set_properties(part, part['PlaybackInfo']['Method'], self.server_id) - - self.kodi_playlist.add(url=part['PlaybackInfo']['Path'], listitem=listitem, index=self.index) + for part, _ in enumerate(self.xml[0][0]): + if part == 0: + # The first part that we've already added + continue + self.api.set_part_number(part) + listitem = widgets.get_listitem(self.xml[0]) + listitem.setSubtitles(self.api.cache_external_subs()) + playqueue_item = PL.playlist_item_from_xml(self.xml[0]) + play = PlayUtils(self.api, playqueue_item) + url = play.getPlayUrl().encode('utf-8') + listitem.setPath(url) + self.kodi_playlist.add(url=url, listitem=listitem, index=self.index) self.index += 1 diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py index 7120a2cd..382fb6ae 100644 --- a/resources/lib/webservice.py +++ b/resources/lib/webservice.py @@ -33,7 +33,7 @@ class WebService(backgroundthread.KillableThread): s.connect(('127.0.0.1', v.WEBSERVICE_PORT)) s.sendall('') except Exception as error: - LOG.error(error) + LOG.error('is_alive error: %s', error) if 'Errno 61' in str(error): alive = False s.close() @@ -47,12 +47,12 @@ class WebService(backgroundthread.KillableThread): conn.request('QUIT', '/') conn.getresponse() except Exception: - pass + utils.ERROR() def run(self): ''' Called to start the webservice. ''' - LOG.info('----===## Starting Webserver on port %s ##===----', + LOG.info('----===## Starting WebService on port %s ##===----', v.WEBSERVICE_PORT) app.APP.register_thread(self) try: @@ -60,11 +60,12 @@ class WebService(backgroundthread.KillableThread): RequestHandler) server.serve_forever() except Exception as error: + LOG.error('Error encountered: %s', error) if '10053' not in error: # ignore host diconnected errors utils.ERROR() finally: app.APP.deregister_thread(self) - LOG.info('##===---- Webserver stopped ----===##') + LOG.info('##===---- WebService stopped ----===##') class HttpServer(BaseHTTPServer.HTTPServer): @@ -75,7 +76,7 @@ class HttpServer(BaseHTTPServer.HTTPServer): self.pending = [] self.threads = [] self.queue = Queue.Queue() - super(HttpServer, self).__init__(*args, **kwargs) + BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) def serve_forever(self): @@ -86,8 +87,9 @@ class HttpServer(BaseHTTPServer.HTTPServer): class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - ''' Http request handler. Do not use LOG here, - it will hang requests in Kodi > show information dialog. + ''' + Http request handler. Do not use LOG here, it will hang requests in Kodi > + show information dialog. ''' timeout = 0.5 @@ -101,8 +103,8 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): ''' try: BaseHTTPServer.BaseHTTPRequestHandler.handle(self) - except Exception: - pass + except Exception as error: + xbmc.log('Plex.WebService handle error: %s' % error, xbmc.LOGWARNING) def do_QUIT(self): ''' send 200 OK response, and set server.stop to True @@ -142,6 +144,7 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def handle_request(self, headers_only=False): '''Send headers and reponse ''' + xbmc.log('Plex.WebService handle_request called. path: %s ]' % self.path, xbmc.LOGWARNING) try: if b'extrafanart' in self.path or b'extrathumbs' in self.path: raise Exception('unsupported artwork request') @@ -161,7 +164,6 @@ class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): except Exception as error: self.send_error(500, b'PLEX.webservice: Exception occurred: %s' % error) - xbmc.log('<[ webservice/%s/%s ]' % (str(id(self)), int(not headers_only)), xbmc.LOGWARNING) def strm(self): ''' Return a dummy video and and queue real items. @@ -268,7 +270,7 @@ class QueuePlay(backgroundthread.KillableThread): params = self.server.queue.get(timeout=0.01) except Queue.Empty: count = 20 - while not utils.window('plex.playlist.ready.bool'): + while not utils.window('plex.playlist.ready'): xbmc.sleep(50) if not count: LOG.info('Playback aborted') @@ -280,14 +282,14 @@ class QueuePlay(backgroundthread.KillableThread): xbmc.executebuiltin('Dialog.Close(busydialognocancel)') play.start_playback() else: - utils.window('plex.playlist.play.bool', True) + utils.window('plex.playlist.play', value='true') xbmc.sleep(1000) play.remove_from_playlist(start_position) break play = PlayStrm(params, params.get('ServerId')) if start_position is None: - start_position = max(play.info['KodiPlaylist'].getposition(), 0) + start_position = max(play.kodi_playlist.getposition(), 0) position = start_position + 1 if play_folder: position = play.play_folder(position) @@ -300,13 +302,13 @@ class QueuePlay(backgroundthread.KillableThread): xbmc.executebuiltin('Activateutils.window(busydialognocancel)') except Exception: utils.ERROR() - play.info['KodiPlaylist'].clear() + play.kodi_playlist.clear() xbmc.Player().stop() self.server.queue.queue.clear() if play_folder: xbmc.executebuiltin('Dialog.Close(busydialognocancel)') else: - utils.window('plex.playlist.aborted.bool', True) + utils.window('plex.playlist.aborted', value='true') break self.server.queue.task_done() diff --git a/resources/lib/widgets.py b/resources/lib/widgets.py index de3e9168..8cfadf6d 100644 --- a/resources/lib/widgets.py +++ b/resources/lib/widgets.py @@ -39,7 +39,7 @@ def get_listitem(xml_element): """ item = generate_item(xml_element) prepare_listitem(item) - return create_listitem(item) + return create_listitem(item, as_tuple=False) def process_method_on_list(method_to_run, items): diff --git a/resources/lib/windows/resume.py b/resources/lib/windows/resume.py new file mode 100644 index 00000000..3f242646 --- /dev/null +++ b/resources/lib/windows/resume.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, unicode_literals +from datetime import timedelta + +import xbmc +import xbmcgui +import xbmcaddon + +from logging import getLogger + + +LOG = getLogger('PLEX.resume') + +XML_PATH = (xbmcaddon.Addon('plugin.video.plexkodiconnect').getAddonInfo('path'), + "default", + "1080i") + +ACTION_PARENT_DIR = 9 +ACTION_PREVIOUS_MENU = 10 +ACTION_BACK = 92 +RESUME = 3010 +START_BEGINNING = 3011 + + +class ResumeDialog(xbmcgui.WindowXMLDialog): + + _resume_point = None + selected_option = None + + def __init__(self, *args, **kwargs): + xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs) + + def set_resume_point(self, time): + self._resume_point = time + + def is_selected(self): + return True if self.selected_option is not None else False + + def get_selected(self): + return self.selected_option + + def onInit(self): + + self.getControl(RESUME).setLabel(self._resume_point) + self.getControl(START_BEGINNING).setLabel(xbmc.getLocalizedString(12021)) + + def onAction(self, action): + if action in (ACTION_BACK, ACTION_PARENT_DIR, ACTION_PREVIOUS_MENU): + self.close() + + def onClick(self, controlID): + if controlID == RESUME: + self.selected_option = 1 + self.close() + if controlID == START_BEGINNING: + self.selected_option = 0 + self.close() + + +def resume_dialog(seconds): + ''' + Base resume dialog based on Kodi settings + Returns True if PKC should resume, False if not, None if user backed out + of the dialog + ''' + LOG.info("Resume dialog called") + dialog = ResumeDialog("script-plex-resume.xml", *XML_PATH) + dialog.set_resume_point("Resume from %s" + % unicode(timedelta(seconds=seconds)).split(".")[0]) + dialog.doModal() + + if dialog.is_selected(): + if not dialog.get_selected(): + # Start from beginning selected + return False + else: + # User backed out + LOG.info("User exited without a selection") + return + return True diff --git a/resources/skins/default/1080i/script-plex-resume.xml b/resources/skins/default/1080i/script-plex-resume.xml new file mode 100644 index 00000000..43240c97 --- /dev/null +++ b/resources/skins/default/1080i/script-plex-resume.xml @@ -0,0 +1,112 @@ + + + 100 + + + + 0 + 0 + 0 + 0 + white.png + stretch + WindowOpen + WindowClose + + + Conditional + + + + + + + + + 50% + 50% + 20% + 90% + + vertical + 0 + 0 + auto + center + 0 + close + close + true + + 30 + + 20 + 100% + 25 + logo-white.png + keep + + + 20 + 100% + 25 + keep + $INFO[Window(Home).Property(EmbyUserImage)] + !String.IsEmpty(Window(Home).Property(EmbyUserImage)) + + + 20 + 100% + 25 + keep + userflyoutdefault.png + String.IsEmpty(Window(Home).Property(EmbyUserImage)) + + + + 100% + 10 + dialogs/menu_top.png + + + 100% + 65 + left + center + 20 + font13 + ffe1e1e1 + ffe1e1e1 + 66000000 + FF404040 + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + + + 100% + 65 + left + center + 20 + font13 + ffe1e1e1 + ffe1e1e1 + 66000000 + FF404040 + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + dialogs/menu_back.png + + + 100% + 10 + dialogs/menu_bottom.png + + + + + + diff --git a/resources/skins/default/media/dialogs/dialog_back.png b/resources/skins/default/media/dialogs/dialog_back.png new file mode 100644 index 0000000000000000000000000000000000000000..6422ff5a20ab4177b062c58afb805a7254d76689 GIT binary patch literal 15141 zcmeI3e{37&8OPsHC|$Z_Xsjg-K^!J6AZp*89VhnHap>YUafy?taT`}7YtMJ*^E)B_@!*Qu^xH~8e?-bmkrOjLINpUbiOfq;X6^kl5muj`- z@^UaXn`sM`lNdW&E$hvKR4C9(wX1Q7a@v}$0_$*5Ep8j@bi3?LYbiU!Hq(roW}B=G z%Q0?_VX4B$;;n{}CoV>~&AyF=a`0EHWymlzj;52zq%G;NsqsOYb-Ud(W2fzQD^ytZ zv|{intD@hWPcoOsC+R|5)(lxyC^Ih~Rue|6#bOpJehT*$(~5-@y}%Aqq*J^`vo?mV zWDE%u@!yVkZP#yz%D-#XV3m2+p3#>aKZ+;Odz zxh+>b#ENH>>B;R}ju*_+%qy51LV|$jwU&lebQWy#|2u*C{D^(=8p$C^xzvWrr^=}o zPok?4Bgx05^@DItT+Uw4XPs{=Pw%14(?2TDpNM?x{P~$%u?y$ZV;*W8Tnlhqd~Oa{ z551tRR5`B?nR6OVZ~j>5 zgAWOkx7q1*I6ZWEbGf5ePD#;_Plo$fH&>+--s1fNX81r+5{6Ei4 zWxf?YEx-pAy7;6rf0m0o;VTYGck~q}M$?fZC=F~(aA6Ul0)h*rfsF|+EFx4uaG^A? zF~NmJgbD~Qlm<2?xUh&&0l|gRz{Ug@77;2SxKJ9{nBc-9LIngDN&_1cTv$Y?fZ#%D zU}J&{iwG4ETqq4}OmJZlp#p*nrGbqJE-WHcKyaZnura}fMT80nE|dl~Cb+POPyxY( z(!j<97ZwpJAh=K(*qGqLB0>cO7fJ&g6I@tCsDR)?X<%c53yTO95L_q?Y)o)r5upNt z3#EaL2`(%mR6uZ{G_Wzjg++u42riTcHohdT>e9P^k^*1&OTst%w%($ggKq^=Lg(fH z0K=;R7`+#O|IEVcM*t)k0RFiH0B#=u*QuH9FK-0kiebNReK0kBc6z-seMjBy_fD|> z`UigY>V^N_@}Xo|u?hUu_F2!W-jlc9ckbsM^*?4liL5-hn{D5<>e%mY1he56>il;4 z*wJ?rABUcpt<9=VvCOMk@Ol5(l4Z4&3z(PDZ0SO|a=&sHQviPzb&JU_S2wn# zm~Sp^|I!aa!lAXtYq|#dr^fUy``b0^&RqHK@n`qjk9>GId-&AZtFAuEYo81lu($L4 z!=f-bdi|+jU+suCeeNGW6(__PYzX!D)y5uiG_KmaHse{dZB4rSd`8^z*p1(N@U3s8 zI@Zmeesd}^`8VgEKHYWTxj%k%$AKAb`lX3IuaCU^hue*8>rBn3FT8&!aQ=yR-fP(V zn$@MBKGk;gnaLCT#A_O|_hv7=;+X0BcW-R`ot~#2U%qML6{JNRRGM+E&<=V@X#J==JgHci>&?)-M(kPy<_Zul(o7- literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_back.png b/resources/skins/default/media/dialogs/menu_back.png new file mode 100644 index 0000000000000000000000000000000000000000..0747aa019a1033df1e6a7ffc65746320b717a520 GIT binary patch literal 15358 zcmeI3O^Do79Kc@{+Pby{zfh!yFs0yECLc4InWUMi+nv^3aO!qDvRf~0CV80+ok?Pn z?aXd3qNkn&K@cg31));#B7)F^TB&#u3MxqPAl@v3mtMSdeJ`1rWZup^MqBXm26mJG z`+vRn`@i>3F1fUP@|FE_&&~k=_Af2gR{+>`5ZxcyyBq!g^abTMx;)}9o(TbX^a=d8 z3w(I_8344aPIE0)8UJK^?T_0Hkpdan~hIt-FTnn}xx0?Iw`d2y5v8uUM zr6D%_1-R}kZUyl4*2$*1b>391+|k;c-q%ooE{qJW-|e`e*01K`xEi|0!$OXWO``MF z+&oUmtu>ap1uuY{k}vV5RH$%eH7_Y@MJ~R?$)Z#eL{*TAyeMg+s)-Ue_~mMI=uQtT zTU)807^Fi|HMbr`z9tC0UN7G(%mIIhw1Hlk`ShZBu{gLQTNQ6e`SutOAuzTpc}UKA!dS?0*c-v~PK)GSkg9oU6# z6e2%qqMyI+MP9h>O-VUYPfd(!(rAowPn>VJJ27<_9lwAO43JLr44a!i6jos9Z3HGf zegQ4z(4^Hzj-3`~EE3jCojdG1Q{1p7af`P?QXw6!Ra*$45qUw=^E$O;mn{zwatjN1 zSCzTvRvp*!dg1fBK)R&$PNTuP5y2Yvtw4va;ZCywQGnS)Wr7d_J zFF@0RwSq288lLD?_sk8vt>;}6hNu`}rbwt%@xeW2w8uw>ijGr5Maf%^rO9PzmqbJ4 zE3#?xt#Yx#S6X(FSBr9~rNFW(+O{s>N*pB{z%=w1B00 zJRb+mr>IsGyChY3rBW#H6{V=~YS~bDC{|2WR>X3v1moQ>?w-zU5PFz>5PJ5dCNs;p z?+E=`;GmXmbTT&oj%Jc#LYN(?79CvMO#(cw#%cKG_-I<&xVid95JKEKs<~nB7q}!vYYFM+lE$6 z7;fON$S0hP`IzN!0OkRYRv3?I^L5vWYVsf(F`V(j5C7!JbQ-;Ld$RwKTdbp=G!TAas%8qVplN3>N`}E;3wnK7^LxB7o3EhKtUJ&@x;E5W2{4 z(fJTshKm3~7a1-(A41D;5kTl7!$s#qXc;a72wh~j=zIt*!$kn0iwqZ?520na2q1Kk z;iB^)vIv+yIa1lW0 zA{EzM`uQnz(Kn|(^l|AYS8m-yAEt8V;z|R6t-}Dk^$q}kZ=>rU0Bnc=+&PE7Y5f3z zr@i;i-aLV9Z!Oj5oBi8={`B!((t$y)?`YyO9e)r3@&$m`Ty0i8DL%-hi!MD5i?fDvC#DnQ&-=0{)m)Kx%i4CTh o&uWKakYRt@a~3UTd;4K<#NK=Hz@Gs^=4k-?9j%NA@z zk(gFuLU(9Ep&a@zV2hh(N?_ScCgaGs9D2H+<^6s?%Q;!6(~cB&V^A}ttX(r^q9-+wPUMEB z$Sh32B-Bg;`S_~7R9rW8Bd(88ve-RlVKgU^NU?U+@g|d1OB?3=l?XurX;r5Y9ZW&C z8yfmRT88siqFb3!?e0xAR+^l$OjggBV~1IF44c)Hx8;^XeuhMJuR>cIN~WHU>UtuW zUu9h*2${AvYgKufyO*e%qGya*B3o%G&37pq3`r&oT24FXvvb}k@3GdS({m5!6gUn` zl2=ybi0FzM8!XGg`=UIuHVa%?4oq2@1MMG0GNmiZf@`rniY&zRbW$>dYEtTlY)b1F z*-|5>EUqe{O;6})G%Of&iEOoI%#~YdLRd3QNt0nX6hsXURaFF^%jI>&6tCUoQY5?I z?{2nBeLmUl_bR-bkMS`t=gvWlL~#44)FE9Suy#u!wQ`lIG5e_0_or2K(n$$SV~zf5 zOH@fSs#Lpbp!GS3ty{Eub7$FN_;7jaNJm1=ok=MvZ9r?s1#BZ*u6T$Q&nU~2+qr@y zTc=r2wl)M*M7FH8G_0btU{n9^1m^Q&_OWYZ;!wNXhRUbPsm4xX=rJ=RrD1D7S`$~Y z*YjD+ZI$Uo);j$YYWYOuTg&HXWW|o2PtiKm1hf{=xTM@1@FF{^tyDR$@3H1IF_6=H zogBJY-uf(9@$O1%@8zv{ccr!Dd_YspptDe|q8W2pCvW~(Sd1PLV6fTaad||xvboaH zrDkEGC#0hNYgnt&=|fvSXR7R}>g+8!c|@dS#5xMb{9Iwb=eeoOx1y&7^q|5PpH#|c zxwsRqaZtLWYn&KQM~mPzh%v=QM1%_{E}RB2rnrcRZ~?`I(;&ta7ZDLIptx`v#F*kD zBEkg}7fyp1Q(QzuxPaoqX%J(Ii--spP+T|-VoY%n5#a)g3#UPhDJ~)+TtIQ*G>9?9 zMMQ)PC@!1^F{ZePh;RYLh0`F$6c-T@E}*z@8pN35A|k>C6cS+ z7~lZ-br}G{1^}A$bxZdxK(8R)6b`jUv&YUJ-P`e5*9~*O{(Iwtt>eFc|F`%4>3ICh zwR4`jbGN$j%%?a1HTBu4?|e`{FMKR<;z#F}+Qh0kCu_3fwl05*Z69uVse9|{B_}6k z>vzBTr&c#RHEzN49mBQz;~i`Fzp(e<(diSP+dh7c5#RRv4BwPp^>v5C!?jPx4}DX2 z*N#mMM{7PjbB9{iw##bYs$4la>r!`*uD@Xm&{Z@*}UO{e(9>2Ejhy?xufDI12x_1hO6S=u!BVl4od aE?oy~P-4!)mf6+|mf`uGp`G)VJ@FsOJ0TVT literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/menu_top.png b/resources/skins/default/media/dialogs/menu_top.png new file mode 100644 index 0000000000000000000000000000000000000000..26bc7d15bafd4e48db9f30f0ee2bbbb9612eb996 GIT binary patch literal 14768 zcmeI3Z)_7~9LFEP2~;pB$Uq@EOAt`oyKB3yceW*C1vazJu@%|ig}dE7-7d6u$K8!~ z3&tU8LeLkY5fEYwMgvCh1;IZd3^4k}pdtE7GyzPMh!|f;NX852bJwnY+I2i&c==qi z_Sfh6et*B`KF^=so7>&dzJ9LfQ4auMZfi@T6M$(;Q0$p?Kl)71x$*=0nr*c7SO7dQ z-}##c-aWJkfXH$+)opht+eBH<`Xxp0gZ_NhKxzP@tMi5=Z-q9~2m4hm=KcHB884$M zG4G~^B$qUr;egsQY{IVL_LMxlRTdQQ>bNJG7g2&Nv?V5=&1jaGk9pm^B8r`6*2}mO zduzDOlO0U6ZbGKPANI+7Fv2tne!fA71VT?U0gex|oWSxSAIFQFAaXoYyu5J_ zilU~H7CRH`isjI+n0LUo4UuJYxtu>2^y_9n%L{_Qasf6F@F9iI8q#bj@6)Vhg(MSs z63~)O)v#4vW1PHFpFU{Eyk4hJ=_=k=)+iOytRg!^kN8_RU@-Ej|kt}(?oIO{RUKg9(^j>D1^ zlvO#Bx}v6sDsu3V6pyT7F;I~MQd`$v&%>5j7CS}czui)r1=N_Je$O8t;EwEiet zZp4(ORV6m-8Qny~g7ILKt<{XV+?6J_YL+c&GHgx6QG;Jq6|`}sa9^;I_l2d9;FJ17 zoUbn(lzmbN%1{vmE)+u9C)p>ZPU!NWvs;R(mAg!h*(arb&Q#G!CuJ~=Gy1zNQ6tTy zQZ1^5*5?qmZqe#>&$2GKt+I8ZBcr-!k|CKEbaq_KJF(?Thgj*1ay;(N6(!j@&Em4N zA)pdvD_YCLDmjZb_5V&_AwOoHx<+mQYPZ`^g;cI;>Liw)wsVpRoBGk3xRbqJ$XaQu zP9J5R(?6qDPDG)#a(*UO?Bw~3I)|Ew)&d%riq)!`F_&}l7LJ83^pF7K;f97_LzJyar$LMGwSae2z`{y`1B@Rvhx_8k=7d>FkI$jB|7NdShI12D1+fU$A({RaSp901pP z0TA~CP_OTO;hS~n6~wyML{ln%@%qIk?JI8Ht5+_4{mS0duYuF!vxg7(H#eQzR{!Gh znW;yPDr0dqw0Fj1?0c`x-mR~?(Vb3@Z_qvO|31%u@WRZEJJRW!`2r`$Fa+2`4i9V>{{SoarwwE zk!xd%TZA^Y=kb5$EE!q6f5Dm$Ml!SlPu>q5PJcT&>rnX0`OznXTh4vbHuh89=cn5avwK!tdh5Wk-os}`p1n|iF8i&z zM-HA?|Hh4d?VEPae`&K|oX;FP`{k`I-_;{pXrVnr8l3SiGG6Cwe5-HYkFV) E58UV(?EnA( literal 0 HcmV?d00001 diff --git a/resources/skins/default/media/dialogs/white.jpg b/resources/skins/default/media/dialogs/white.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1206155018137636cf6c1c658768acce3283c22 GIT binary patch literal 8060 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 VJoC;o|NMQa{Mros_ze8yzX3F67cu|< literal 0 HcmV?d00001