From 48e2f5bdfb9aceb891b9aa204c365bc60cb43c46 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 02:13:13 +0200 Subject: [PATCH 1/9] wip --- plugins/system/CoreApi/CoreApi.py | 27 ++ .../controller/ContentApiController.py | 257 ++++++++++++++++++ .../controller/PlaylistApiController.py | 99 +++++++ .../CoreApi/controller/SlideApiController.py | 184 +++++++++++++ .../exception/ContentNotFoundException.py | 11 + .../exception/ContentPathMissingException.py | 11 + .../exception/FolderNotEmptyException.py | 11 + .../exception/FolderNotFoundException.py | 11 + plugins/system/CoreApi/lang/en.json | 4 + plugins/system/CoreApi/lang/es.json | 4 + plugins/system/CoreApi/lang/fr.json | 4 + plugins/system/CoreApi/lang/it.json | 4 + .../CoreUpdater.py} | 5 +- .../controller/CoreUpdaterController.py} | 4 +- .../{GitUpdater => CoreUpdater}/lang/en.json | 2 +- .../{GitUpdater => CoreUpdater}/lang/es.json | 2 +- .../{GitUpdater => CoreUpdater}/lang/fr.json | 2 +- .../{GitUpdater => CoreUpdater}/lang/it.json | 2 +- .../views/update_button.jinja.html | 4 + .../GitUpdater/views/update_button.jinja.html | 4 - plugins/user/Dashboard/Dashboard.py | 3 + src/controller/ContentController.py | 26 +- src/exceptions/HttpClientException.py | 5 + src/interface/ObPlugin.py | 4 + src/manager/ContentManager.py | 9 +- src/manager/DatabaseManager.py | 1 + src/manager/FolderManager.py | 8 +- src/manager/NodePlayerManager.py | 2 +- src/manager/SlideManager.py | 9 +- src/model/entity/Slide.py | 19 +- src/service/WebServer.py | 26 +- src/util/utils.py | 25 ++ 32 files changed, 752 insertions(+), 37 deletions(-) create mode 100644 plugins/system/CoreApi/CoreApi.py create mode 100644 plugins/system/CoreApi/controller/ContentApiController.py create mode 100644 plugins/system/CoreApi/controller/PlaylistApiController.py create mode 100644 plugins/system/CoreApi/controller/SlideApiController.py create mode 100644 plugins/system/CoreApi/exception/ContentNotFoundException.py create mode 100644 plugins/system/CoreApi/exception/ContentPathMissingException.py create mode 100644 plugins/system/CoreApi/exception/FolderNotEmptyException.py create mode 100644 plugins/system/CoreApi/exception/FolderNotFoundException.py create mode 100644 plugins/system/CoreApi/lang/en.json create mode 100644 plugins/system/CoreApi/lang/es.json create mode 100644 plugins/system/CoreApi/lang/fr.json create mode 100644 plugins/system/CoreApi/lang/it.json rename plugins/system/{GitUpdater/GitUpdater.py => CoreUpdater/CoreUpdater.py} (91%) rename plugins/system/{GitUpdater/controller/GitUpdaterController.py => CoreUpdater/controller/CoreUpdaterController.py} (94%) rename plugins/system/{GitUpdater => CoreUpdater}/lang/en.json (73%) rename plugins/system/{GitUpdater => CoreUpdater}/lang/es.json (72%) rename plugins/system/{GitUpdater => CoreUpdater}/lang/fr.json (76%) rename plugins/system/{GitUpdater => CoreUpdater}/lang/it.json (73%) create mode 100644 plugins/system/CoreUpdater/views/update_button.jinja.html delete mode 100644 plugins/system/GitUpdater/views/update_button.jinja.html create mode 100644 src/exceptions/HttpClientException.py diff --git a/plugins/system/CoreApi/CoreApi.py b/plugins/system/CoreApi/CoreApi.py new file mode 100644 index 0000000..a5558f7 --- /dev/null +++ b/plugins/system/CoreApi/CoreApi.py @@ -0,0 +1,27 @@ +from src.interface.ObPlugin import ObPlugin + +from typing import List, Dict +from src.model.entity.Variable import Variable +from src.model.enum.HookType import HookType +from src.model.hook.HookRegistration import HookRegistration + + +class CoreApi(ObPlugin): + + def get_version(self) -> str: + return '1.0' + + def use_id(self): + return 'core_api' + + def use_title(self): + return self.translate('plugin_title') + + def use_description(self): + return self.translate('plugin_description') + + def use_variables(self) -> List[Variable]: + return [] + + def use_hooks_registrations(self) -> List[HookRegistration]: + return [] diff --git a/plugins/system/CoreApi/controller/ContentApiController.py b/plugins/system/CoreApi/controller/ContentApiController.py new file mode 100644 index 0000000..12fe891 --- /dev/null +++ b/plugins/system/CoreApi/controller/ContentApiController.py @@ -0,0 +1,257 @@ +import json +import os +import time + +from flask import Flask, request, jsonify, abort +from werkzeug.utils import secure_filename +from src.service.ModelStore import ModelStore +from src.model.entity.Content import Content +from src.manager.FolderManager import FolderManager +from src.model.enum.ContentType import ContentType +from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH +from src.interface.ObController import ObController +from src.service.ExternalStorageServer import ExternalStorageServer +from src.util.utils import str_to_enum, get_optional_string +from src.util.UtilFile import randomize_filename +from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException +from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException +from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException +from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException + + +class ContentApiController(ObController): + + def register(self): + self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET']) + self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST']) + self._app.add_url_rule('/api/content/', 'api_content_get', self.get_content, methods=['GET']) + self._app.add_url_rule('/api/content/', 'api_content_update', self.update_content, methods=['PUT']) + self._app.add_url_rule('/api/content/', 'api_content_delete', self.delete_content, methods=['DELETE']) + self._app.add_url_rule('/api/content/rename', 'api_content_rename', self.rename_content, methods=['PUT']) + self._app.add_url_rule('/api/content/location/', 'api_content_location', self.location_content, methods=['GET']) + self._app.add_url_rule('/api/content/move-bulk', 'api_content_bulk_move', self.move_bulk_content, methods=['POST']) + self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST']) + self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST']) + self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE']) + self._app.add_url_rule('/api/content/folder/rename', 'api_folder_rename', self.rename_folder, methods=['PUT']) + + def get_json(self): + data = {} + + try: + data = request.get_json() + except: + pass + + return data + + def get_folder_context(self): + data = self.get_json() + path = data.get('path') + path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if not path.startswith(FOLDER_ROOT_PATH) else path + + if 'folder_id' in data: + folder = self._model_store.folder().get(id=data.get('folder_id')) + return path, folder + + if 'path' not in data: + raise ContentPathMissingException() + + folder = self._model_store.folder().get_one_by_path(path=path, entity=FolderEntity.CONTENT) + is_root_drive = FolderManager.is_root_drive(path) + + if not folder and not is_root_drive: + raise FolderNotFoundException() + + return FOLDER_ROOT_PATH if is_root_drive else path, folder + + def list_content(self): + data = self.get_json() + working_folder_path = None + working_folder = None + folder_id = None + + try: + working_folder_path, working_folder = self.get_folder_context() + folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id) + except ContentPathMissingException: + pass + + contents = self._model_store.content().get_contents( + folder_id=folder_id, + slide_id=data.get('slide_id', None), + ) + result = [content.to_dict() for content in contents] + + return jsonify({ + 'contents': result, + 'working_folder_path': working_folder_path, + 'working_folder': working_folder.to_dict() if working_folder else None + }) + + def get_content(self, content_id: int): + content = self._model_store.content().get(content_id) + if not content: + raise ContentNotFoundException() + + return jsonify(content.to_dict()) + + def add_content(self): + data = self.get_json() + working_folder_path, working_folder = self.get_folder_context() + + if 'name' not in data or 'type' not in data: + abort(400, description="Name and type are required") + + content_type = str_to_enum(data.get('type'), ContentType) + location = data.get('location', None) + + content = self._model_store.content().add_form_raw( + name=data.get('name'), + type=content_type, + request_files=request.files, + upload_dir=self._app.config['UPLOAD_FOLDER'], + location=location, + folder_id=working_folder.id if working_folder else None + ) + + if not content: + abort(400, description="Failed to add content") + + return jsonify(content.to_dict()), 201 + + def upload_bulk_content(self): + working_folder_path, working_folder = self.get_folder_context() + + for key in request.files: + files = request.files.getlist(key) + for file in files: + content_type = ContentType.guess_content_type_file(file.filename) + name = file.filename.rsplit('.', 1)[0] + + if content_type: + self._model_store.content().add_form_raw( + name=name, + type=content_type, + request_files=file, + upload_dir=self._app.config['UPLOAD_FOLDER'], + folder_id=working_folder.id if working_folder else None + ) + + return jsonify({'status': 'ok'}), 201 + + def update_content(self, content_id: int): + data = self.get_json() + content = self._model_store.content().get(content_id) + + if not content: + raise ContentNotFoundException() + + if 'name' not in data: + abort(400, description="Name is required") + + self._model_store.content().update_form( + id=content.id, + name=data.get('name'), + ) + self._post_update() + + return jsonify(content.to_dict()) + + def delete_content(self, content_id: int): + content = self._model_store.content().get(content_id) + + if not content: + raise ContentNotFoundException() + + if self._model_store.slide().count_slides_for_content(content.id) > 0: + abort(400, description="Content is referenced in slides") + + self._model_store.content().delete(content.id) + self._post_update() + + return jsonify({'status': 'ok'}), 204 + + def rename_content(self): + data = self.get_json() + + if 'id' not in data or 'name' not in data: + abort(400, description="ID and name are required") + + if not self._model_store.content().get(data.get('id')): + raise ContentNotFoundException() + + self._model_store.content().update_form( + id=data.get('id'), + name=data.get('name') + ) + + return jsonify({'status': 'ok'}) + + def move_bulk_content(self): + data = self.get_json() + + if 'entity_ids' not in data or 'new_folder_id' not in data: + abort(400, description="Entity IDs and new folder ID are required") + + entity_ids = data.get('entity_ids') + + for entity_id in entity_ids: + self._model_store.folder().move_to_folder( + entity_id=entity_id, + folder_id=data.get('new_folder_id'), + entity_is_folder=False, + entity=FolderEntity.CONTENT + ) + + return jsonify({'status': 'ok'}) + + def location_content(self, content_id: int): + content = self._model_store.content().get(content_id) + + if not content: + raise ContentNotFoundException() + + content_location = self._model_store.content().resolve_content_location(content) + + return jsonify({'location': content_location}) + + def add_folder(self): + data = self.get_json() + working_folder_path, working_folder = self.get_folder_context() + + folder = self._model_store.folder().add_folder( + entity=FolderEntity.CONTENT, + name=data.get('name'), + working_folder_path=working_folder_path + ) + + return jsonify(folder.to_dict()), 201 + + def delete_folder(self): + working_folder_path, working_folder = self.get_folder_context() + + content_counter = self._model_store.content().count_contents_for_folder(working_folder.id) + folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id) + + if content_counter > 0 or folder_counter: + raise FolderNotEmptyException() + + self._model_store.folder().delete(id=working_folder.id) + self._post_update() + + return jsonify({'status': 'ok'}), 204 + + def rename_folder(self): + data = self.get_json() + working_folder_path, working_folder = self.get_folder_context() + + self._model_store.folder().rename_folder( + folder_id=working_folder.id, + name=data.get('name') + ) + + return jsonify({'status': 'ok'}) + + def _post_update(self): + self._model_store.variable().update_by_name("last_content_update", time.time()) diff --git a/plugins/system/CoreApi/controller/PlaylistApiController.py b/plugins/system/CoreApi/controller/PlaylistApiController.py new file mode 100644 index 0000000..2468f23 --- /dev/null +++ b/plugins/system/CoreApi/controller/PlaylistApiController.py @@ -0,0 +1,99 @@ +from flask import Flask, render_template, jsonify, request, abort, make_response + +from src.model.entity.Playlist import Playlist +from src.interface.ObController import ObController + + +class PlaylistApiController(ObController): + + def register(self): + self._app.add_url_rule('/api/playlist', 'api_playlist_list', self.get_playlists, methods=['GET']) + self._app.add_url_rule('/api/playlist', 'api_playlist_add', self.add_playlist, methods=['POST']) + self._app.add_url_rule('/api/playlist/', 'api_playlist_get', self.get_playlist, methods=['GET']) + self._app.add_url_rule('/api/playlist/', 'api_playlist_update', self.update_playlist, methods=['PUT']) + self._app.add_url_rule('/api/playlist/', 'api_playlist_delete', self.delete_playlist, methods=['DELETE']) + self._app.add_url_rule('/api/playlist//slides', 'api_playlist_list_slides', self.get_playlists_slides, methods=['GET']) + self._app.add_url_rule('/api/playlist//notifications', 'api_playlist_list_notifications', self.get_playlists_notifications, methods=['GET']) + + def get_playlists(self): + playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True) + result = [playlist.to_dict() for playlist in playlists] + return jsonify(result) + + def get_playlist(self, playlist_id: int): + playlist = self._model_store.playlist().get(playlist_id) + if not playlist: + abort(404, description="Playlist not found") + return jsonify(playlist.to_dict()) + + def add_playlist(self): + data = request.get_json() + if not data or 'name' not in data: + abort(400, description="Invalid input") + + playlist = Playlist( + name=data.get('name'), + enabled=data.get('enabled', True), + time_sync=data.get('time_sync', False) + ) + + try: + playlist = self._model_store.playlist().add_form(playlist) + except Exception as e: + abort(409, description=str(e)) + + return jsonify(playlist.to_dict()), 201 + + def update_playlist(self, playlist_id: int): + data = request.get_json() + if not data or 'name' not in data: + abort(400, description="Invalid input") + + playlist = self._model_store.playlist().get(playlist_id) + if not playlist: + abort(404, description="Playlist not found") + + self._model_store.playlist().update_form( + id=playlist_id, + name=data.get('name', playlist.name), + time_sync=data.get('time_sync', playlist.time_sync), + enabled=data.get('enabled', playlist.enabled) + ) + updated_playlist = self._model_store.playlist().get(playlist_id) + return jsonify(updated_playlist.to_dict()) + + def delete_playlist(self, playlist_id: int): + playlist = self._model_store.playlist().get(playlist_id) + if not playlist: + abort(404, description="Playlist not found") + + if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0: + abort(400, description="Playlist cannot be deleted because it has slides") + + if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0: + abort(400, description="Playlist cannot be deleted because it is associated with node player groups") + + self._model_store.playlist().delete(playlist_id) + return '', 204 + + def get_playlists_slides(self, playlist_id: int): + playlist = self._model_store.playlist().get(playlist_id) + + if not playlist: + abort(404, description="Playlist not found") + + slides = self._model_store.slide().get_slides(is_notification=False, playlist_id=playlist_id) + + result = [slide.to_dict() for slide in slides] + return jsonify(result) + + def get_playlists_notifications(self, playlist_id: int): + playlist = self._model_store.playlist().get(playlist_id) + + if not playlist: + abort(404, description="Playlist not found") + + slides = self._model_store.slide().get_slides(is_notification=True, playlist_id=playlist_id) + + result = [slide.to_dict() for slide in slides] + return jsonify(result) diff --git a/plugins/system/CoreApi/controller/SlideApiController.py b/plugins/system/CoreApi/controller/SlideApiController.py new file mode 100644 index 0000000..0e9159d --- /dev/null +++ b/plugins/system/CoreApi/controller/SlideApiController.py @@ -0,0 +1,184 @@ +import json +import os +import time +from datetime import datetime + +from flask import Flask, request, jsonify, abort, make_response +from werkzeug.utils import secure_filename +from src.service.ModelStore import ModelStore +from src.model.entity.Slide import Slide +from src.model.enum.ContentType import ContentType +from src.interface.ObController import ObController +from src.util.utils import str_to_enum, get_optional_string, str_datetime_to_cron, str_weekdaytime_to_cron +from src.util.UtilFile import randomize_filename + + +class SlideApiController(ObController): + + def register(self): + self._app.add_url_rule('/api/slide', 'api_slide_add', self.add_slide, methods=['POST']) + self._app.add_url_rule('/api/slide/notification', 'api_slide_notification_add', self.add_notification, methods=['POST']) + self._app.add_url_rule('/api/slide/', 'api_slide_get', self.get_slide, methods=['GET']) + self._app.add_url_rule('/api/slide/', 'api_slide_edit', self.edit_slide, methods=['PUT']) + self._app.add_url_rule('/api/slide/', 'api_slide_delete', self.delete_slide, methods=['DELETE']) + self._app.add_url_rule('/api/slide/positions', 'api_slide_positions', self.update_slide_positions, methods=['POST']) + + def get_slide(self, slide_id: int): + slide = self._model_store.slide().get(slide_id) + if not slide: + abort(404, description="Slide not found") + return jsonify(slide.to_dict()) + + def add_slide(self): + return self.add_slide_or_notification(is_notification=False) + + def add_notification(self): + return self.add_slide_or_notification(is_notification=True) + + def add_slide_or_notification(self, is_notification=False): + data = request.get_json() + + if not data or 'content_id' not in data: + abort(400, description="Valid Content ID is required") + + if not self._model_store.content().get(data.get('content_id')): + abort(404, description="Content not found") + + if not data or 'playlist_id' not in data: + abort(400, description="Valid Playlist ID is required") + + if not self._model_store.playlist().get(data.get('playlist_id')): + abort(404, description="Playlist not found") + + cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=is_notification) + + slide = Slide( + content_id=data.get('content_id'), + enabled=data.get('enabled', True), + delegate_duration=data.get('delegate_duration', False), + duration=data.get('duration', 3), + position=data.get('position', 999), + is_notification=is_notification, + playlist_id=data.get('playlist_id', None), + cron_schedule=cron_schedule_start, + cron_schedule_end=cron_schedule_end + ) + + slide = self._model_store.slide().add_form(slide) + self._post_update() + + return jsonify(slide.to_dict()), 201 + + def edit_slide(self, slide_id: int): + data = request.get_json() + if not data or 'content_id' not in data: + abort(400, description="Content ID is required") + + slide = self._model_store.slide().get(slide_id) + if not slide: + abort(404, description="Slide not found") + + cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=slide.is_notification) + + self._model_store.slide().update_form( + id=slide_id, + content_id=data.get('content_id', slide.content_id), + enabled=data.get('enabled', slide.enabled), + position=data.get('position', slide.position), + delegate_duration=data.get('delegate_duration', slide.delegate_duration), + duration=data.get('duration', slide.duration), + cron_schedule=cron_schedule_start if 'scheduling' in data else slide.cron_schedule, + cron_schedule_end=cron_schedule_end if 'scheduling' in data else slide.cron_schedule_end, + ) + self._post_update() + + updated_slide = self._model_store.slide().get(slide_id) + return jsonify(updated_slide.to_dict()) + + def delete_slide(self, slide_id: int): + slide = self._model_store.slide().get(slide_id) + + if not slide: + abort(404, description="Slide not found") + + self._model_store.slide().delete(slide_id) + self._post_update() + + return '', 204 + + def update_slide_positions(self): + data = request.get_json() + if not data: + abort(400, description="Positions data are required") + + self._model_store.slide().update_positions(data) + self._post_update() + return jsonify({'status': 'ok'}) + + def _post_update(self): + self._model_store.variable().update_by_name("last_slide_update", time.time()) + + def _resolve_scheduling(self, data, is_notification=False): + try: + return self._resolve_scheduling_for_notification(data) if is_notification else self._resolve_scheduling_for_slide(data) + except ValueError as ve: + abort(400, description=str(ve)) + + def _resolve_scheduling_for_slide(self, data): + scheduling = data.get('scheduling', 'loop') + cron_schedule_start = None + cron_schedule_end = None + + if scheduling == 'loop': + pass + elif scheduling == 'datetime': + datetime_start = data.get('datetime_start') + datetime_end = data.get('datetime_end') + + if not datetime_start: + abort(400, description="Field datetime_start is required for scheduling='datetime'") + + cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start) + + if datetime_end: + cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end) + elif scheduling == 'inweek': + day_start = data.get('day_start') + time_start = data.get('time_start') + day_end = data.get('day_end') + time_end = data.get('time_end') + + if not (day_start and time_start and day_end and time_end): + abort(400, description="day_start, time_start, day_end, and time_end are required for scheduling='inweek'") + cron_schedule_start = str_weekdaytime_to_cron(weekday=int(day_start), time_str=time_start) + cron_schedule_end = str_weekdaytime_to_cron(weekday=int(day_end), time_str=time_end) + else: + abort(400, description="Invalid value for slide scheduling. Expected 'loop', 'datetime', or 'inweek'.") + + return cron_schedule_start, cron_schedule_end + + def _resolve_scheduling_for_notification(self, data): + scheduling = data.get('scheduling', 'datetime') + cron_schedule_start = None + cron_schedule_end = None + + if scheduling == 'datetime': + datetime_start = data.get('datetime_start') + datetime_end = data.get('datetime_end') + + if not datetime_start: + abort(400, description="Field datetime_start is required for scheduling='datetime'") + + cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start) + + if datetime_end: + cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end) + elif scheduling == 'cron': + cron_schedule_start = data.get('cron_start') + + if not cron_schedule_start: + abort(400, description="Field cron_start is required for scheduling='cron'") + else: + abort(400, description="Invalid value for notification scheduling. Expected 'datetime' or 'cron'.") + + return cron_schedule_start, cron_schedule_end diff --git a/plugins/system/CoreApi/exception/ContentNotFoundException.py b/plugins/system/CoreApi/exception/ContentNotFoundException.py new file mode 100644 index 0000000..5217fc0 --- /dev/null +++ b/plugins/system/CoreApi/exception/ContentNotFoundException.py @@ -0,0 +1,11 @@ +from src.exceptions.HttpClientException import HttpClientException + + +class ContentNotFoundException(HttpClientException): + @property + def code(self) -> int: + return 404 + + @property + def description(self) -> int: + return "Content not found" diff --git a/plugins/system/CoreApi/exception/ContentPathMissingException.py b/plugins/system/CoreApi/exception/ContentPathMissingException.py new file mode 100644 index 0000000..bc48c61 --- /dev/null +++ b/plugins/system/CoreApi/exception/ContentPathMissingException.py @@ -0,0 +1,11 @@ +from src.exceptions.HttpClientException import HttpClientException + + +class ContentPathMissingException(HttpClientException): + @property + def code(self) -> int: + return 400 + + @property + def description(self) -> int: + return "Valid path is required" diff --git a/plugins/system/CoreApi/exception/FolderNotEmptyException.py b/plugins/system/CoreApi/exception/FolderNotEmptyException.py new file mode 100644 index 0000000..243523a --- /dev/null +++ b/plugins/system/CoreApi/exception/FolderNotEmptyException.py @@ -0,0 +1,11 @@ +from src.exceptions.HttpClientException import HttpClientException + + +class FolderNotEmptyException(HttpClientException): + @property + def code(self) -> int: + return 400 + + @property + def description(self) -> int: + return "Folder is not empty" diff --git a/plugins/system/CoreApi/exception/FolderNotFoundException.py b/plugins/system/CoreApi/exception/FolderNotFoundException.py new file mode 100644 index 0000000..c2a33e7 --- /dev/null +++ b/plugins/system/CoreApi/exception/FolderNotFoundException.py @@ -0,0 +1,11 @@ +from src.exceptions.HttpClientException import HttpClientException + + +class FolderNotFoundException(HttpClientException): + @property + def code(self) -> int: + return 404 + + @property + def description(self) -> int: + return "Folder not found" diff --git a/plugins/system/CoreApi/lang/en.json b/plugins/system/CoreApi/lang/en.json new file mode 100644 index 0000000..231fe24 --- /dev/null +++ b/plugins/system/CoreApi/lang/en.json @@ -0,0 +1,4 @@ +{ + "plugin_title": "Core API", + "plugin_description": "Adds api feature wrapping core features" +} diff --git a/plugins/system/CoreApi/lang/es.json b/plugins/system/CoreApi/lang/es.json new file mode 100644 index 0000000..91c3d59 --- /dev/null +++ b/plugins/system/CoreApi/lang/es.json @@ -0,0 +1,4 @@ +{ + "plugin_title": "Core API", + "plugin_description": "Agrega características de API que envuelven las características principales" +} \ No newline at end of file diff --git a/plugins/system/CoreApi/lang/fr.json b/plugins/system/CoreApi/lang/fr.json new file mode 100644 index 0000000..83b67e6 --- /dev/null +++ b/plugins/system/CoreApi/lang/fr.json @@ -0,0 +1,4 @@ +{ + "plugin_title": "Core API", + "plugin_description": "Ajoute des fonctionnalités d'API englobant les fonctionnalités principales" +} \ No newline at end of file diff --git a/plugins/system/CoreApi/lang/it.json b/plugins/system/CoreApi/lang/it.json new file mode 100644 index 0000000..de1a4a3 --- /dev/null +++ b/plugins/system/CoreApi/lang/it.json @@ -0,0 +1,4 @@ +{ + "plugin_title": "Core API", + "plugin_description": "Aggiunge funzionalità API che racchiudono le funzionalità di base" +} \ No newline at end of file diff --git a/plugins/system/GitUpdater/GitUpdater.py b/plugins/system/CoreUpdater/CoreUpdater.py similarity index 91% rename from plugins/system/GitUpdater/GitUpdater.py rename to plugins/system/CoreUpdater/CoreUpdater.py index b8178cc..787be47 100644 --- a/plugins/system/GitUpdater/GitUpdater.py +++ b/plugins/system/CoreUpdater/CoreUpdater.py @@ -10,8 +10,11 @@ from src.util.utils import am_i_in_docker class GitUpdater(ObPlugin): + def get_version(self) -> str: + return '1.0' + def use_id(self): - return 'git_updater' + return 'core_updater' def use_title(self): return self.translate('plugin_title') diff --git a/plugins/system/GitUpdater/controller/GitUpdaterController.py b/plugins/system/CoreUpdater/controller/CoreUpdaterController.py similarity index 94% rename from plugins/system/GitUpdater/controller/GitUpdaterController.py rename to plugins/system/CoreUpdater/controller/CoreUpdaterController.py index fc580b3..5e10adc 100644 --- a/plugins/system/GitUpdater/controller/GitUpdaterController.py +++ b/plugins/system/CoreUpdater/controller/CoreUpdaterController.py @@ -10,10 +10,10 @@ from src.util.utils import run_system_command, sudo_run_system_command, get_work from src.Application import Application -class GitUpdaterController(ObController): +class CoreUpdaterController(ObController): def register(self): - self._app.add_url_rule('/git-updater/update/now', 'git_updater_update_now', self._auth(self.update_now), methods=['GET']) + self._app.add_url_rule('/core-updater/update/now', 'core_updater_update_now', self._auth(self.update_now), methods=['GET']) def update_now(self): debug = self._model_store.config().map().get('debug') diff --git a/plugins/system/GitUpdater/lang/en.json b/plugins/system/CoreUpdater/lang/en.json similarity index 73% rename from plugins/system/GitUpdater/lang/en.json rename to plugins/system/CoreUpdater/lang/en.json index b3a087a..497e335 100644 --- a/plugins/system/GitUpdater/lang/en.json +++ b/plugins/system/CoreUpdater/lang/en.json @@ -1,5 +1,5 @@ { - "plugin_title": "Git Updater Button", + "plugin_title": "Core Updater Button", "plugin_description": "Adds an update button (only for system-wide installations)", "button_update": "Update" } diff --git a/plugins/system/GitUpdater/lang/es.json b/plugins/system/CoreUpdater/lang/es.json similarity index 72% rename from plugins/system/GitUpdater/lang/es.json rename to plugins/system/CoreUpdater/lang/es.json index e49a92f..8762564 100644 --- a/plugins/system/GitUpdater/lang/es.json +++ b/plugins/system/CoreUpdater/lang/es.json @@ -1,5 +1,5 @@ { - "plugin_title": "Botón de Actualización de Git", + "plugin_title": "Core Updater Button", "plugin_description": "Añade un botón de actualización (solo para instalaciones a nivel del sistema)", "button_update": "Actualizar" } diff --git a/plugins/system/GitUpdater/lang/fr.json b/plugins/system/CoreUpdater/lang/fr.json similarity index 76% rename from plugins/system/GitUpdater/lang/fr.json rename to plugins/system/CoreUpdater/lang/fr.json index bd9e0ee..fb82edf 100644 --- a/plugins/system/GitUpdater/lang/fr.json +++ b/plugins/system/CoreUpdater/lang/fr.json @@ -1,5 +1,5 @@ { - "plugin_title": "Bouton de mise à jour", + "plugin_title": "Core Updater Button", "plugin_description": "Ajoute un bouton de mise à jour (seulement pour les installations système)", "button_update": "Mettre à jour" } diff --git a/plugins/system/GitUpdater/lang/it.json b/plugins/system/CoreUpdater/lang/it.json similarity index 73% rename from plugins/system/GitUpdater/lang/it.json rename to plugins/system/CoreUpdater/lang/it.json index 96b1dde..1da0409 100644 --- a/plugins/system/GitUpdater/lang/it.json +++ b/plugins/system/CoreUpdater/lang/it.json @@ -1,5 +1,5 @@ { - "plugin_title": "Pulsante di aggiornamento", + "plugin_title": "Core Updater Button", "plugin_description": "Aggiunge un pulsante di aggiornamento (solo per installazioni di sistema)", "button_update": "Aggiorna" } diff --git a/plugins/system/CoreUpdater/views/update_button.jinja.html b/plugins/system/CoreUpdater/views/update_button.jinja.html new file mode 100644 index 0000000..3c493b9 --- /dev/null +++ b/plugins/system/CoreUpdater/views/update_button.jinja.html @@ -0,0 +1,4 @@ +{% if not am_i_in_docker() %} + {{ l.core_updater_button_update }} +{% endif %} + diff --git a/plugins/system/GitUpdater/views/update_button.jinja.html b/plugins/system/GitUpdater/views/update_button.jinja.html deleted file mode 100644 index fb205fc..0000000 --- a/plugins/system/GitUpdater/views/update_button.jinja.html +++ /dev/null @@ -1,4 +0,0 @@ -{% if not am_i_in_docker() %} - {{ l.git_updater_button_update }} -{% endif %} - diff --git a/plugins/user/Dashboard/Dashboard.py b/plugins/user/Dashboard/Dashboard.py index d77ebfb..b85dcc7 100644 --- a/plugins/user/Dashboard/Dashboard.py +++ b/plugins/user/Dashboard/Dashboard.py @@ -8,6 +8,9 @@ from src.model.hook.HookRegistration import HookRegistration class Dashboard(ObPlugin): + def get_version(self) -> str: + return '1.0' + def use_id(self): return 'dashboard' diff --git a/src/controller/ContentController.py b/src/controller/ContentController.py index c6c0c85..1f7a521 100644 --- a/src/controller/ContentController.py +++ b/src/controller/ContentController.py @@ -32,7 +32,7 @@ class ContentController(ObController): self._app.add_url_rule('/slideshow/content/upload-bulk', 'slideshow_content_upload_bulk', self._auth(self.slideshow_content_upload_bulk), methods=['POST']) self._app.add_url_rule('/slideshow/content/delete-bulk-explr', 'slideshow_content_delete_bulk_explr', self._auth(self.slideshow_content_delete_bulk_explr), methods=['GET']) - def get_working_folder(self): + def get_folder_context(self): working_folder_path = request.args.get('path', None) working_folder = None @@ -47,7 +47,7 @@ class ContentController(ObController): def slideshow_content_list(self): self._model_store.variable().update_by_name('last_pillmenu_slideshow', 'slideshow_content_list') - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() slides_with_content = self._model_store.slide().get_all_indexed(attribute='content_id', multiple=True) return render_template( @@ -64,7 +64,7 @@ class ContentController(ObController): ) def slideshow_content_add(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() route_args = { "path": working_folder_path, } @@ -86,7 +86,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_list', **route_args)) def slideshow_content_upload_bulk(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() for key in request.files: files = request.files.getlist(key) @@ -111,7 +111,7 @@ class ContentController(ObController): if not content: return abort(404) - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() return render_template( 'slideshow/contents/edit.jinja.html', @@ -123,7 +123,7 @@ class ContentController(ObController): ) def slideshow_content_save(self, content_id: int = 0): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() content = self._model_store.content().get(content_id) if not content: @@ -139,7 +139,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_edit', content_id=content_id, saved=1)) def slideshow_content_delete(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() error_tuple = self.delete_content_by_id(request.args.get('id')) route_args = { "path": working_folder_path, @@ -151,7 +151,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_list', **route_args)) def slideshow_content_rename(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() self._model_store.content().update_form( id=request.form['id'], name=request.form['name'], @@ -182,7 +182,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_list', path=path)) def slideshow_content_folder_add(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() self._model_store.folder().add_folder( entity=FolderEntity.CONTENT, @@ -193,7 +193,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_list', path=working_folder_path)) def slideshow_content_folder_rename(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() self._model_store.folder().rename_folder( folder_id=request.form['id'], name=request.form['name'], @@ -202,7 +202,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_list', path=working_folder_path)) def slideshow_content_folder_move(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() entity_ids = request.form['entity_ids'].split(',') folder_ids = request.form['folder_ids'].split(',') @@ -225,7 +225,7 @@ class ContentController(ObController): return redirect(url_for('slideshow_content_list', path=working_folder_path)) def slideshow_content_folder_delete(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() error_tuple = self.delete_folder_by_id(request.args.get('id')) route_args = { "path": working_folder_path, @@ -245,7 +245,7 @@ class ContentController(ObController): return redirect(self._model_store.content().resolve_content_location(content)) def slideshow_content_delete_bulk_explr(self): - working_folder_path, working_folder = self.get_working_folder() + working_folder_path, working_folder = self.get_folder_context() entity_ids = request.args.get('entity_ids', '').split(',') folder_ids = request.args.get('folder_ids', '').split(',') route_args_dict = {"path": working_folder_path} diff --git a/src/exceptions/HttpClientException.py b/src/exceptions/HttpClientException.py new file mode 100644 index 0000000..99f9999 --- /dev/null +++ b/src/exceptions/HttpClientException.py @@ -0,0 +1,5 @@ +from werkzeug.exceptions import HTTPException + + +class HttpClientException(HTTPException): + pass diff --git a/src/interface/ObPlugin.py b/src/interface/ObPlugin.py index 8d2e992..54bca9f 100644 --- a/src/interface/ObPlugin.py +++ b/src/interface/ObPlugin.py @@ -46,6 +46,10 @@ class ObPlugin(abc.ABC): def use_hooks_registrations(self) -> List[HookRegistration]: pass + @abc.abstractmethod + def get_version(self) -> str: + pass + def get_directory(self) -> Optional[str]: return self._plugin_dir diff --git a/src/manager/ContentManager.py b/src/manager/ContentManager.py index 3edca22..c43eb97 100644 --- a/src/manager/ContentManager.py +++ b/src/manager/ContentManager.py @@ -92,14 +92,17 @@ class ContentManager(ModelManager): for content_id, edits in edits_contents.items(): self._db.update_by_id(self.TABLE_NAME, content_id, edits) - def get_contents(self, slide_id: Optional[id] = None, folder_id: Optional[id] = None) -> List[Content]: + def get_contents(self, slide_id: Optional[int] = None, folder_id: Optional[int] = None) -> List[Content]: query = " 1=1 " if slide_id: query = "{} {}".format(query, "AND slide_id = {}".format(slide_id)) - if folder_id: - query = "{} {}".format(query, "AND folder_id = {}".format(folder_id)) + if isinstance(folder_id, int): + if folder_id == 0: + query = "{} {}".format(query, "AND folder_id is null") + else: + query = "{} {}".format(query, "AND folder_id = {}".format(folder_id)) return self.get_by(query=query) diff --git a/src/manager/DatabaseManager.py b/src/manager/DatabaseManager.py index 87e19f7..999c59e 100644 --- a/src/manager/DatabaseManager.py +++ b/src/manager/DatabaseManager.py @@ -215,6 +215,7 @@ class DatabaseManager: "DELETE FROM settings WHERE name = 'playlist_enabled'", "UPDATE fleet_player_group SET slug = id WHERE slug = '' or slug is null", "UPDATE content SET uuid = id WHERE uuid = '' or uuid is null", + "UPDATE slide SET uuid = id WHERE uuid = '' or uuid is null", ] for query in queries: diff --git a/src/manager/FolderManager.py b/src/manager/FolderManager.py index 5fc69b1..2a8aa31 100644 --- a/src/manager/FolderManager.py +++ b/src/manager/FolderManager.py @@ -110,7 +110,7 @@ class FolderManager(ModelManager): for folder_id, edits in edits_folders.items(): self._db.update_by_id(self.TABLE_NAME, folder_id, edits) - def get_folders(self, parent_id: Optional[id] = None) -> List[Folder]: + def get_folders(self, parent_id: Optional[int] = None) -> List[Folder]: query = " 1=1 " if parent_id: @@ -267,3 +267,9 @@ class FolderManager(ModelManager): def count_subfolders_for_folder(self, folder_id: int) -> int: return len(self.get_folders(parent_id=folder_id)) + + @staticmethod + def is_root_drive(path: str): + clean_path = path.strip('/') + clean_root_path = FOLDER_ROOT_PATH.strip('/') + return path == '/' or clean_path == clean_root_path diff --git a/src/manager/NodePlayerManager.py b/src/manager/NodePlayerManager.py index 9130964..e90affd 100644 --- a/src/manager/NodePlayerManager.py +++ b/src/manager/NodePlayerManager.py @@ -81,7 +81,7 @@ class NodePlayerManager(ModelManager): for node_player_id, edits in edits_node_players.items(): self._db.update_by_id(self.TABLE_NAME, node_player_id, edits) - def get_node_players(self, group_id: Optional[int] = None, folder_id: Optional[id] = None, sort: Optional[str] = None, ascending=False) -> List[NodePlayer]: + def get_node_players(self, group_id: Optional[int] = None, folder_id: Optional[int] = None, sort: Optional[str] = None, ascending=False) -> List[NodePlayer]: query = " 1=1 " if group_id: diff --git a/src/manager/SlideManager.py b/src/manager/SlideManager.py index b8e1286..15eb17e 100644 --- a/src/manager/SlideManager.py +++ b/src/manager/SlideManager.py @@ -16,6 +16,7 @@ class SlideManager(ModelManager): TABLE_NAME = "slides" TABLE_MODEL = [ + "uuid CHAR(255)", "enabled INTEGER DEFAULT 0", "delegate_duration INTEGER DEFAULT 0", "is_notification INTEGER DEFAULT 0", @@ -136,7 +137,7 @@ class SlideManager(ModelManager): for slide_id, slide_position in positions.items(): self._db.update_by_id(self.TABLE_NAME, slide_id, {"position": slide_position}) - def update_form(self, id: int, duration: Optional[int] = None, content_id: Optional[int] = None, delegate_duration: Optional[bool] = None, is_notification: bool = False, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', enabled: Optional[bool] = None) -> Optional[Slide]: + def update_form(self, id: int, duration: Optional[int] = None, content_id: Optional[int] = None, delegate_duration: Optional[bool] = None, is_notification: Optional[bool] = None, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', enabled: Optional[bool] = None, position: Optional[int] = None) -> Optional[Slide]: slide = self.get(id) if not slide: @@ -145,9 +146,10 @@ class SlideManager(ModelManager): form = { "duration": duration if duration else slide.duration, "content_id": content_id if content_id else slide.content_id, + "position": position if isinstance(position, int) else slide.position, "enabled": enabled if isinstance(enabled, bool) else slide.enabled, "delegate_duration": delegate_duration if isinstance(delegate_duration, bool) else slide.delegate_duration, - "is_notification": True if is_notification else False, + "is_notification": is_notification if isinstance(is_notification, bool) else slide.is_notification, "cron_schedule": get_optional_string(cron_schedule), "cron_schedule_end": get_optional_string(cron_schedule_end) } @@ -156,7 +158,7 @@ class SlideManager(ModelManager): self.post_update(id) return self.get(id) - def add_form(self, slide: Union[Slide, Dict]) -> None: + def add_form(self, slide: Union[Slide, Dict]) -> Slide: form = slide if not isinstance(slide, dict): @@ -165,6 +167,7 @@ class SlideManager(ModelManager): self._db.add(self.TABLE_NAME, self.pre_add(form)) self.post_add(slide.id) + return self.get_one_by(query="uuid = '{}'".format(slide.uuid)) def delete(self, id: int) -> None: slide = self.get(id) diff --git a/src/model/entity/Slide.py b/src/model/entity/Slide.py index f020964..7fb39ad 100644 --- a/src/model/entity/Slide.py +++ b/src/model/entity/Slide.py @@ -1,5 +1,6 @@ import json import time +import uuid from typing import Optional, Union from src.util.utils import str_to_enum @@ -7,7 +8,8 @@ from src.util.utils import str_to_enum class Slide: - def __init__(self, playlist_id: Optional[int] = None, content_id: Optional[int] = None, delegate_duration=False, duration: int = 3, is_notification: bool = False, enabled: bool = False, position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None): + def __init__(self, uuid: str = '', playlist_id: Optional[int] = None, content_id: Optional[int] = None, delegate_duration=False, duration: int = 3, is_notification: bool = False, enabled: bool = False, position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None): + self._uuid = uuid if uuid else self.generate_and_set_uuid() self._id = id if id else None self._playlist_id = playlist_id self._content_id = content_id @@ -23,10 +25,23 @@ class Slide: self._created_at = int(created_at if created_at else time.time()) self._updated_at = int(updated_at if updated_at else time.time()) + def generate_and_set_uuid(self) -> str: + self._uuid = str(uuid.uuid4()) + + return self._uuid + @property def id(self) -> Optional[int]: return self._id + @property + def uuid(self) -> str: + return self._uuid + + @uuid.setter + def uuid(self, value: str): + self._uuid = value + @property def created_by(self) -> str: return self._created_by @@ -134,6 +149,7 @@ class Slide: def __str__(self) -> str: return f"Slide(" \ f"id='{self.id}',\n" \ + f"uuid='{self.uuid}',\n" \ f"enabled='{self.enabled}',\n" \ f"is_notification='{self.is_notification}',\n" \ f"duration='{self.duration}',\n" \ @@ -160,6 +176,7 @@ class Slide: def to_dict(self) -> dict: slide = { "id": self.id, + "uuid": self.uuid, "enabled": self.enabled, "is_notification": self.is_notification, "position": self.position, diff --git a/src/service/WebServer.py b/src/service/WebServer.py index 3ece8b4..5d3418e 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -2,7 +2,7 @@ import os import time from waitress import serve -from flask import Flask, send_from_directory, redirect, url_for +from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response from flask_login import LoginManager, current_user from src.manager.UserManager import UserManager @@ -19,6 +19,8 @@ from src.controller.SysinfoController import SysinfoController from src.controller.SettingsController import SettingsController from src.controller.CoreController import CoreController from src.constant.WebDirConstant import WebDirConstant +from src.exceptions.HttpClientException import HttpClientException +from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException class WebServer: @@ -119,7 +121,23 @@ class WebServer: return self._template_renderer.get_view_globals() def _setup_web_errors(self) -> None: - @self._app.errorhandler(404) - def not_found(e): - return send_from_directory(self.get_template_dir(), 'core/error404.html'), 404 + def handle_error(error): + if request.headers.get('Content-Type') == 'application/json': + response = jsonify({ + 'error': { + 'code': error.code, + 'message': error.description + } + }) + return make_response(response, error.code) + + if error.code == 404: + return send_from_directory(self.get_template_dir(), 'core/error404.html'), 404 + + return error + + self._app.register_error_handler(400, handle_error) + self._app.register_error_handler(404, handle_error) + self._app.register_error_handler(409, handle_error) + self._app.register_error_handler(HttpClientException, handle_error) diff --git a/src/util/utils.py b/src/util/utils.py index 5b97021..59c9b5b 100644 --- a/src/util/utils.py +++ b/src/util/utils.py @@ -59,6 +59,31 @@ def camel_to_snake(camel: str) -> str: return CAMEL_CASE_TO_SNAKE_CASE_PATTERN.sub('_', camel).lower() +def str_datetime_to_cron(datetime_str: str) -> str: + print(datetime_str) + datetime_obj = datetime.strptime(datetime_str, '%Y-%m-%d %H:%M') + return "{} {} {} {} * {}".format( + datetime_obj.minute, + datetime_obj.hour, + datetime_obj.day, + datetime_obj.month, + datetime_obj.year + ) + + +def str_weekdaytime_to_cron(weekday: int, time_str: str) -> str: + if weekday < 1 or weekday > 7: + raise ValueError('Weekday must be between [1-7]') + + time_obj = datetime.strptime(time_str, '%H:%M').time() + + return "{} {} * * {}".format( + time_obj.minute, + time_obj.hour, + weekday + ) + + def is_cron_in_datetime_moment(expression: str) -> bool: pattern = re.compile(r'^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\*\s+(\d+)$') return bool(pattern.match(expression)) From 46a05cde84fec7e58584f5ddd4acfb392c5a6c58 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 02:43:57 +0200 Subject: [PATCH 2/9] working content api --- .../controller/ContentApiController.py | 174 +++++++++--------- src/service/WebServer.py | 2 +- 2 files changed, 91 insertions(+), 85 deletions(-) diff --git a/plugins/system/CoreApi/controller/ContentApiController.py b/plugins/system/CoreApi/controller/ContentApiController.py index 12fe891..dc50869 100644 --- a/plugins/system/CoreApi/controller/ContentApiController.py +++ b/plugins/system/CoreApi/controller/ContentApiController.py @@ -27,28 +27,30 @@ class ContentApiController(ObController): self._app.add_url_rule('/api/content/', 'api_content_get', self.get_content, methods=['GET']) self._app.add_url_rule('/api/content/', 'api_content_update', self.update_content, methods=['PUT']) self._app.add_url_rule('/api/content/', 'api_content_delete', self.delete_content, methods=['DELETE']) - self._app.add_url_rule('/api/content/rename', 'api_content_rename', self.rename_content, methods=['PUT']) self._app.add_url_rule('/api/content/location/', 'api_content_location', self.location_content, methods=['GET']) - self._app.add_url_rule('/api/content/move-bulk', 'api_content_bulk_move', self.move_bulk_content, methods=['POST']) self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST']) + self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST']) self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST']) self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE']) - self._app.add_url_rule('/api/content/folder/rename', 'api_folder_rename', self.rename_folder, methods=['PUT']) + self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT']) - def get_json(self): + def get_request_data(self): data = {} try: - data = request.get_json() + if 'multipart/form-data' in request.headers.get('Content-Type'): + data = request.form + else: + data = request.get_json() except: pass return data def get_folder_context(self): - data = self.get_json() + data = self.get_request_data() path = data.get('path') - path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if not path.startswith(FOLDER_ROOT_PATH) else path + path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if path and not path.startswith(FOLDER_ROOT_PATH) else path if 'folder_id' in data: folder = self._model_store.folder().get(id=data.get('folder_id')) @@ -66,7 +68,7 @@ class ContentApiController(ObController): return FOLDER_ROOT_PATH if is_root_drive else path, folder def list_content(self): - data = self.get_json() + data = self.get_request_data() working_folder_path = None working_folder = None folder_id = None @@ -89,19 +91,18 @@ class ContentApiController(ObController): 'working_folder': working_folder.to_dict() if working_folder else None }) - def get_content(self, content_id: int): - content = self._model_store.content().get(content_id) - if not content: - raise ContentNotFoundException() - - return jsonify(content.to_dict()) - def add_content(self): - data = self.get_json() + data = self.get_request_data() working_folder_path, working_folder = self.get_folder_context() - if 'name' not in data or 'type' not in data: - abort(400, description="Name and type are required") + if 'name' not in data: + abort(400, description="Name is required") + + if 'type' not in data: + abort(400, description="Type is required") + + if data.get('type') not in {type.value for type in ContentType}: + abort(400, description="Invalid type") content_type = str_to_enum(data.get('type'), ContentType) location = data.get('location', None) @@ -120,6 +121,56 @@ class ContentApiController(ObController): return jsonify(content.to_dict()), 201 + def get_content(self, content_id: int): + content = self._model_store.content().get(content_id) + if not content: + raise ContentNotFoundException() + + return jsonify(content.to_dict()) + + def update_content(self, content_id: int): + data = self.get_request_data() + content = self._model_store.content().get(content_id) + + if not content: + raise ContentNotFoundException() + + if 'name' not in data: + abort(400, description="Name is required") + + content = self._model_store.content().update_form( + id=content.id, + name=data.get('name'), + ) + + self._post_update() + + return jsonify(content.to_dict()) + + def delete_content(self, content_id: int): + content = self._model_store.content().get(content_id) + + if not content: + raise ContentNotFoundException() + + if self._model_store.slide().count_slides_for_content(content.id) > 0: + abort(400, description="Content is referenced in slides") + + self._model_store.content().delete(content.id) + self._post_update() + + return jsonify({'status': 'ok'}), 204 + + def location_content(self, content_id: int): + content = self._model_store.content().get(content_id) + + if not content: + raise ContentNotFoundException() + + content_location = self._model_store.content().resolve_content_location(content) + + return jsonify({'location': content_location}) + def upload_bulk_content(self): working_folder_path, working_folder = self.get_folder_context() @@ -140,86 +191,32 @@ class ContentApiController(ObController): return jsonify({'status': 'ok'}), 201 - def update_content(self, content_id: int): - data = self.get_json() - content = self._model_store.content().get(content_id) + def move_bulk_content_folder(self): + data = self.get_request_data() + working_folder_path, working_folder = self.get_folder_context() - if not content: - raise ContentNotFoundException() - - if 'name' not in data: - abort(400, description="Name is required") - - self._model_store.content().update_form( - id=content.id, - name=data.get('name'), - ) - self._post_update() - - return jsonify(content.to_dict()) - - def delete_content(self, content_id: int): - content = self._model_store.content().get(content_id) - - if not content: - raise ContentNotFoundException() - - if self._model_store.slide().count_slides_for_content(content.id) > 0: - abort(400, description="Content is referenced in slides") - - self._model_store.content().delete(content.id) - self._post_update() - - return jsonify({'status': 'ok'}), 204 - - def rename_content(self): - data = self.get_json() - - if 'id' not in data or 'name' not in data: - abort(400, description="ID and name are required") - - if not self._model_store.content().get(data.get('id')): - raise ContentNotFoundException() - - self._model_store.content().update_form( - id=data.get('id'), - name=data.get('name') - ) - - return jsonify({'status': 'ok'}) - - def move_bulk_content(self): - data = self.get_json() - - if 'entity_ids' not in data or 'new_folder_id' not in data: - abort(400, description="Entity IDs and new folder ID are required") + if 'entity_ids' not in data: + abort(400, description="Content IDs are required under 'entity_ids' field") entity_ids = data.get('entity_ids') for entity_id in entity_ids: self._model_store.folder().move_to_folder( entity_id=entity_id, - folder_id=data.get('new_folder_id'), + folder_id=working_folder if working_folder else None, entity_is_folder=False, entity=FolderEntity.CONTENT ) return jsonify({'status': 'ok'}) - def location_content(self, content_id: int): - content = self._model_store.content().get(content_id) - - if not content: - raise ContentNotFoundException() - - content_location = self._model_store.content().resolve_content_location(content) - - return jsonify({'location': content_location}) - def add_folder(self): - data = self.get_json() + data = self.get_request_data() working_folder_path, working_folder = self.get_folder_context() + if 'name' not in data: + abort(400, description="Name is required") + folder = self._model_store.folder().add_folder( entity=FolderEntity.CONTENT, name=data.get('name'), @@ -231,6 +228,9 @@ class ContentApiController(ObController): def delete_folder(self): working_folder_path, working_folder = self.get_folder_context() + if not working_folder: + abort(400, description="You can't update this folder") + content_counter = self._model_store.content().count_contents_for_folder(working_folder.id) folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id) @@ -242,10 +242,16 @@ class ContentApiController(ObController): return jsonify({'status': 'ok'}), 204 - def rename_folder(self): - data = self.get_json() + def update_folder(self): + data = self.get_request_data() working_folder_path, working_folder = self.get_folder_context() + if 'name' not in data: + abort(400, description="Name is required") + + if not working_folder: + abort(400, description="You can't update this folder") + self._model_store.folder().rename_folder( folder_id=working_folder.id, name=data.get('name') diff --git a/src/service/WebServer.py b/src/service/WebServer.py index 5d3418e..eb79cc9 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -122,7 +122,7 @@ class WebServer: def _setup_web_errors(self) -> None: def handle_error(error): - if request.headers.get('Content-Type') == 'application/json': + if request.headers.get('Content-Type') == 'application/json' or request.headers.get('Accept') == 'application/json': response = jsonify({ 'error': { 'code': error.code, From c7fc29ba9943d306f78423873d9d1eaea01aa46c Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 03:55:54 +0200 Subject: [PATCH 3/9] wip swagger --- .../controller/ContentApiController.py | 207 ++++++++++----- .../controller/PlaylistApiController.py | 96 +++++-- .../CoreApi/controller/SlideApiController.py | 236 ++++++++++++------ requirements.txt | 1 + src/interface/ObController.py | 3 + src/manager/ConfigManager.py | 4 +- src/manager/PlaylistManager.py | 2 +- src/service/WebServer.py | 18 ++ 8 files changed, 393 insertions(+), 174 deletions(-) diff --git a/plugins/system/CoreApi/controller/ContentApiController.py b/plugins/system/CoreApi/controller/ContentApiController.py index dc50869..c9335eb 100644 --- a/plugins/system/CoreApi/controller/ContentApiController.py +++ b/plugins/system/CoreApi/controller/ContentApiController.py @@ -1,38 +1,78 @@ -import json -import os -import time - -from flask import Flask, request, jsonify, abort -from werkzeug.utils import secure_filename -from src.service.ModelStore import ModelStore +from flask import request, abort, jsonify +from flask_restx import Resource, Namespace, fields from src.model.entity.Content import Content from src.manager.FolderManager import FolderManager from src.model.enum.ContentType import ContentType from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH from src.interface.ObController import ObController -from src.service.ExternalStorageServer import ExternalStorageServer -from src.util.utils import str_to_enum, get_optional_string -from src.util.UtilFile import randomize_filename +from src.util.utils import str_to_enum from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException +# Namespace for content operations +content_ns = Namespace('content', description='Operations related to content management') + +# Input model for adding/updating content +content_model = content_ns.model('Content', { + 'name': fields.String(required=True, description='Name of the content'), + 'type': fields.String(required=True, description='Type of the content'), + 'location': fields.String(description='Location of the content (optional)'), + 'path': fields.String(description='Path of the folder') +}) + +# Output model for content +content_output_model = content_ns.model('ContentOutput', { + 'id': fields.Integer(readOnly=True, description='Unique identifier of the content'), + 'name': fields.String(description='Name of the content'), + 'type': fields.String(description='Type of the content'), + 'location': fields.String(description='Location of the content'), + 'folder_id': fields.Integer(description='Folder ID where the content is stored') +}) + +# Model for folder operations +folder_model = content_ns.model('Folder', { + 'name': fields.String(required=True, description='Name of the folder'), + 'path': fields.String(description='Path of the folder') +}) + +# Model for bulk operations +bulk_move_model = content_ns.model('BulkMove', { + 'entity_ids': fields.List(fields.Integer, required=True, description='List of content IDs to move'), + 'path': fields.String(description='Path of the folder') +}) + + class ContentApiController(ObController): def register(self): - self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET']) - self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST']) - self._app.add_url_rule('/api/content/', 'api_content_get', self.get_content, methods=['GET']) - self._app.add_url_rule('/api/content/', 'api_content_update', self.update_content, methods=['PUT']) - self._app.add_url_rule('/api/content/', 'api_content_delete', self.delete_content, methods=['DELETE']) - self._app.add_url_rule('/api/content/location/', 'api_content_location', self.location_content, methods=['GET']) - self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST']) - self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST']) - self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST']) - self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE']) - self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT']) +# - self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET']) +# - self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST']) +# - self._app.add_url_rule('/api/content/', 'api_content_get', self.get_content, methods=['GET']) +# - self._app.add_url_rule('/api/content/', 'api_content_update', self.update_content, methods=['PUT']) +# - self._app.add_url_rule('/api/content/', 'api_content_delete', self.delete_content, methods=['DELETE']) +# - self._app.add_url_rule('/api/content/location/', 'api_content_location', self.location_content, methods=['GET']) +# - self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST']) +# - self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST']) +# - self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST']) +# - self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE']) +# - self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT']) + self.api().add_namespace(content_ns, path='/api/content') + content_ns.add_resource(self.create_resource(ContentListResource), '/') + content_ns.add_resource(self.create_resource(ContentResource), '/') + content_ns.add_resource(self.create_resource(ContentLocationResource), '/location/') + content_ns.add_resource(self.create_resource(ContentBulkUploadResource), '/upload-bulk') + content_ns.add_resource(self.create_resource(FolderBulkMoveResource), '/folder/move-bulk') + content_ns.add_resource(self.create_resource(FolderResource), '/folder') + + def create_resource(self, resource_class): + # Function to inject dependencies into resources + return type(f'{resource_class.__name__}WithDependencies', (resource_class,), { + '_model_store': self._model_store, + '_controller': self + }) def get_request_data(self): data = {} @@ -67,14 +107,19 @@ class ContentApiController(ObController): return FOLDER_ROOT_PATH if is_root_drive else path, folder - def list_content(self): - data = self.get_request_data() + +class ContentListResource(Resource): + + @content_ns.marshal_list_with(content_output_model) + def get(self): + """List all contents""" + data = self._controller.get_request_data() working_folder_path = None working_folder = None folder_id = None try: - working_folder_path, working_folder = self.get_folder_context() + working_folder_path, working_folder = self._controller.get_folder_context() folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id) except ContentPathMissingException: pass @@ -85,15 +130,18 @@ class ContentApiController(ObController): ) result = [content.to_dict() for content in contents] - return jsonify({ + return { 'contents': result, 'working_folder_path': working_folder_path, 'working_folder': working_folder.to_dict() if working_folder else None - }) + } - def add_content(self): - data = self.get_request_data() - working_folder_path, working_folder = self.get_folder_context() + @content_ns.expect(content_model) + @content_ns.marshal_with(content_output_model, code=201) + def post(self): + """Add new content""" + data = self._controller.get_request_data() + working_folder_path, working_folder = self._controller.get_folder_context() if 'name' not in data: abort(400, description="Name is required") @@ -111,7 +159,7 @@ class ContentApiController(ObController): name=data.get('name'), type=content_type, request_files=request.files, - upload_dir=self._app.config['UPLOAD_FOLDER'], + upload_dir=self._controller._app.config['UPLOAD_FOLDER'], location=location, folder_id=working_folder.id if working_folder else None ) @@ -119,17 +167,25 @@ class ContentApiController(ObController): if not content: abort(400, description="Failed to add content") - return jsonify(content.to_dict()), 201 + return content.to_dict(), 201 - def get_content(self, content_id: int): + +class ContentResource(Resource): + + @content_ns.marshal_with(content_output_model) + def get(self, content_id: int): + """Get content by ID""" content = self._model_store.content().get(content_id) if not content: raise ContentNotFoundException() - return jsonify(content.to_dict()) + return content.to_dict() - def update_content(self, content_id: int): - data = self.get_request_data() + @content_ns.expect(content_model) + @content_ns.marshal_with(content_output_model) + def put(self, content_id: int): + """Update existing content""" + data = self._controller.get_request_data() content = self._model_store.content().get(content_id) if not content: @@ -143,11 +199,12 @@ class ContentApiController(ObController): name=data.get('name'), ) - self._post_update() + self._controller._post_update() - return jsonify(content.to_dict()) + return content.to_dict() - def delete_content(self, content_id: int): + def delete(self, content_id: int): + """Delete content""" content = self._model_store.content().get(content_id) if not content: @@ -157,11 +214,15 @@ class ContentApiController(ObController): abort(400, description="Content is referenced in slides") self._model_store.content().delete(content.id) - self._post_update() + self._controller._post_update() - return jsonify({'status': 'ok'}), 204 + return {'status': 'ok'}, 204 - def location_content(self, content_id: int): + +class ContentLocationResource(Resource): + + def get(self, content_id: int): + """Get content location by ID""" content = self._model_store.content().get(content_id) if not content: @@ -169,10 +230,14 @@ class ContentApiController(ObController): content_location = self._model_store.content().resolve_content_location(content) - return jsonify({'location': content_location}) + return {'location': content_location} - def upload_bulk_content(self): - working_folder_path, working_folder = self.get_folder_context() + +class ContentBulkUploadResource(Resource): + + def post(self): + """Upload multiple content files""" + working_folder_path, working_folder = self._controller.get_folder_context() for key in request.files: files = request.files.getlist(key) @@ -185,15 +250,20 @@ class ContentApiController(ObController): name=name, type=content_type, request_files=file, - upload_dir=self._app.config['UPLOAD_FOLDER'], + upload_dir=self._controller._app.config['UPLOAD_FOLDER'], folder_id=working_folder.id if working_folder else None ) - return jsonify({'status': 'ok'}), 201 + return {'status': 'ok'}, 201 - def move_bulk_content_folder(self): - data = self.get_request_data() - working_folder_path, working_folder = self.get_folder_context() + +class FolderBulkMoveResource(Resource): + + @content_ns.expect(bulk_move_model) + def post(self): + """Move multiple content to another folder""" + data = self._controller.get_request_data() + working_folder_path, working_folder = self._controller.get_folder_context() if 'entity_ids' not in data: abort(400, description="Content IDs are required under 'entity_ids' field") @@ -208,11 +278,17 @@ class ContentApiController(ObController): entity=FolderEntity.CONTENT ) - return jsonify({'status': 'ok'}) + return {'status': 'ok'} - def add_folder(self): - data = self.get_request_data() - working_folder_path, working_folder = self.get_folder_context() + +class FolderResource(Resource): + + @content_ns.expect(folder_model) + @content_ns.marshal_with(folder_model, code=201) + def post(self): + """Add a new folder""" + data = self._controller.get_request_data() + working_folder_path, working_folder = self._controller.get_folder_context() if 'name' not in data: abort(400, description="Name is required") @@ -223,13 +299,14 @@ class ContentApiController(ObController): working_folder_path=working_folder_path ) - return jsonify(folder.to_dict()), 201 + return folder.to_dict(), 201 - def delete_folder(self): - working_folder_path, working_folder = self.get_folder_context() + def delete(self): + """Delete a folder""" + working_folder_path, working_folder = self._controller.get_folder_context() if not working_folder: - abort(400, description="You can't update this folder") + abort(400, description="You can't delete this folder") content_counter = self._model_store.content().count_contents_for_folder(working_folder.id) folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id) @@ -238,13 +315,15 @@ class ContentApiController(ObController): raise FolderNotEmptyException() self._model_store.folder().delete(id=working_folder.id) - self._post_update() + self._controller._post_update() - return jsonify({'status': 'ok'}), 204 + return {'status': 'ok'}, 204 - def update_folder(self): - data = self.get_request_data() - working_folder_path, working_folder = self.get_folder_context() + @content_ns.expect(folder_model) + def put(self): + """Update a folder""" + data = self._controller.get_request_data() + working_folder_path, working_folder = self._controller.get_folder_context() if 'name' not in data: abort(400, description="Name is required") @@ -257,7 +336,5 @@ class ContentApiController(ObController): name=data.get('name') ) - return jsonify({'status': 'ok'}) + return {'status': 'ok'} - def _post_update(self): - self._model_store.variable().update_by_name("last_content_update", time.time()) diff --git a/plugins/system/CoreApi/controller/PlaylistApiController.py b/plugins/system/CoreApi/controller/PlaylistApiController.py index 2468f23..b586952 100644 --- a/plugins/system/CoreApi/controller/PlaylistApiController.py +++ b/plugins/system/CoreApi/controller/PlaylistApiController.py @@ -1,32 +1,55 @@ -from flask import Flask, render_template, jsonify, request, abort, make_response - +from flask import request, abort, jsonify +from flask_restx import Resource, Namespace, fields from src.model.entity.Playlist import Playlist from src.interface.ObController import ObController +# Namespace pour les opérations sur les playlists +playlist_ns = Namespace('playlists', description='Playlist operations') + +# Modèle d'entrée pour la playlist +playlist_model = playlist_ns.model('Playlist', { + 'name': fields.String(required=True, description='The playlist name'), + 'enabled': fields.Boolean(default=True, description='Is the playlist enabled?'), + 'time_sync': fields.Boolean(default=False, description='Is time synchronization enabled?') +}) + +# Modèle de sortie pour la playlist +playlist_output_model = playlist_ns.model('PlaylistOutput', { + 'id': fields.Integer(readOnly=True, description='The unique identifier of a playlist'), + 'name': fields.String(required=True, description='The playlist name'), + 'enabled': fields.Boolean(description='Is the playlist enabled?'), + 'time_sync': fields.Boolean(description='Is time synchronization enabled?') +}) + class PlaylistApiController(ObController): def register(self): - self._app.add_url_rule('/api/playlist', 'api_playlist_list', self.get_playlists, methods=['GET']) - self._app.add_url_rule('/api/playlist', 'api_playlist_add', self.add_playlist, methods=['POST']) - self._app.add_url_rule('/api/playlist/', 'api_playlist_get', self.get_playlist, methods=['GET']) - self._app.add_url_rule('/api/playlist/', 'api_playlist_update', self.update_playlist, methods=['PUT']) - self._app.add_url_rule('/api/playlist/', 'api_playlist_delete', self.delete_playlist, methods=['DELETE']) - self._app.add_url_rule('/api/playlist//slides', 'api_playlist_list_slides', self.get_playlists_slides, methods=['GET']) - self._app.add_url_rule('/api/playlist//notifications', 'api_playlist_list_notifications', self.get_playlists_notifications, methods=['GET']) + self.api().add_namespace(playlist_ns, path='/api/playlists') + playlist_ns.add_resource(self.create_resource(PlaylistResource), '/') + playlist_ns.add_resource(self.create_resource(PlaylistListResource), '/') + playlist_ns.add_resource(self.create_resource(PlaylistSlidesResource), '//slides') + playlist_ns.add_resource(self.create_resource(PlaylistNotificationsResource), '//notifications') - def get_playlists(self): + def create_resource(self, resource_class): + # Function to inject dependencies into resources + return type(f'{resource_class.__name__}WithDependencies', (resource_class,), { + '_model_store': self._model_store + }) + + +class PlaylistListResource(Resource): + @playlist_ns.marshal_list_with(playlist_output_model) + def get(self): + """List all playlists""" playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True) result = [playlist.to_dict() for playlist in playlists] - return jsonify(result) + return result - def get_playlist(self, playlist_id: int): - playlist = self._model_store.playlist().get(playlist_id) - if not playlist: - abort(404, description="Playlist not found") - return jsonify(playlist.to_dict()) - - def add_playlist(self): + @playlist_ns.expect(playlist_model) + @playlist_ns.marshal_with(playlist_output_model, code=201) + def post(self): + """Create a new playlist""" data = request.get_json() if not data or 'name' not in data: abort(400, description="Invalid input") @@ -42,12 +65,24 @@ class PlaylistApiController(ObController): except Exception as e: abort(409, description=str(e)) - return jsonify(playlist.to_dict()), 201 + return playlist.to_dict(), 201 - def update_playlist(self, playlist_id: int): + +class PlaylistResource(Resource): + + @playlist_ns.marshal_with(playlist_output_model) + def get(self, playlist_id): + """Get a playlist by its ID""" + playlist = self._model_store.playlist().get(playlist_id) + if not playlist: + abort(404, description="Playlist not found") + return playlist.to_dict() + + @playlist_ns.expect(playlist_model) + @playlist_ns.marshal_with(playlist_output_model) + def put(self, playlist_id): + """Update an existing playlist""" data = request.get_json() - if not data or 'name' not in data: - abort(400, description="Invalid input") playlist = self._model_store.playlist().get(playlist_id) if not playlist: @@ -60,9 +95,10 @@ class PlaylistApiController(ObController): enabled=data.get('enabled', playlist.enabled) ) updated_playlist = self._model_store.playlist().get(playlist_id) - return jsonify(updated_playlist.to_dict()) + return updated_playlist.to_dict() - def delete_playlist(self, playlist_id: int): + def delete(self, playlist_id): + """Delete a playlist""" playlist = self._model_store.playlist().get(playlist_id) if not playlist: abort(404, description="Playlist not found") @@ -76,7 +112,11 @@ class PlaylistApiController(ObController): self._model_store.playlist().delete(playlist_id) return '', 204 - def get_playlists_slides(self, playlist_id: int): + +class PlaylistSlidesResource(Resource): + + def get(self, playlist_id): + """Get slides associated with a playlist""" playlist = self._model_store.playlist().get(playlist_id) if not playlist: @@ -87,7 +127,11 @@ class PlaylistApiController(ObController): result = [slide.to_dict() for slide in slides] return jsonify(result) - def get_playlists_notifications(self, playlist_id: int): + +class PlaylistNotificationsResource(Resource): + + def get(self, playlist_id): + """Get notifications associated with a playlist""" playlist = self._model_store.playlist().get(playlist_id) if not playlist: diff --git a/plugins/system/CoreApi/controller/SlideApiController.py b/plugins/system/CoreApi/controller/SlideApiController.py index 0e9159d..605c889 100644 --- a/plugins/system/CoreApi/controller/SlideApiController.py +++ b/plugins/system/CoreApi/controller/SlideApiController.py @@ -1,43 +1,69 @@ -import json -import os -import time -from datetime import datetime - -from flask import Flask, request, jsonify, abort, make_response -from werkzeug.utils import secure_filename -from src.service.ModelStore import ModelStore +from flask import request, abort, jsonify +from flask_restx import Resource, Namespace, fields from src.model.entity.Slide import Slide -from src.model.enum.ContentType import ContentType from src.interface.ObController import ObController -from src.util.utils import str_to_enum, get_optional_string, str_datetime_to_cron, str_weekdaytime_to_cron -from src.util.UtilFile import randomize_filename +from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron +import time + +# Namespace for slide operations +slide_ns = Namespace('slides', description='Operations on slides') + +# Input model for adding/editing a slide +slide_model = slide_ns.model('Slide', { + 'content_id': fields.Integer(required=True, description='The content ID for the slide'), + 'playlist_id': fields.Integer(required=True, description='The playlist ID to which the slide belongs'), + 'enabled': fields.Boolean(default=True, description='Is the slide enabled?'), + 'delegate_duration': fields.Boolean(default=False, description='Should the duration be delegated?'), + 'duration': fields.Integer(default=3, description='Duration of the slide'), + 'position': fields.Integer(default=999, description='Position of the slide'), + 'scheduling': fields.String(description='Scheduling type: loop, datetime, or inweek'), + 'datetime_start': fields.String(description='Start datetime for scheduling'), + 'datetime_end': fields.String(description='End datetime for scheduling'), + 'day_start': fields.Integer(description='Start day for inweek scheduling'), + 'time_start': fields.String(description='Start time for inweek scheduling'), + 'day_end': fields.Integer(description='End day for inweek scheduling'), + 'time_end': fields.String(description='End time for inweek scheduling'), + 'cron_start': fields.String(description='Cron expression for scheduling start'), + 'cron_end': fields.String(description='Cron expression for scheduling end'), +}) + +# Output model for a slide +slide_output_model = slide_ns.model('SlideOutput', { + 'id': fields.Integer(readOnly=True, description='The unique identifier of a slide'), + 'content_id': fields.Integer(description='The content ID for the slide'), + 'playlist_id': fields.Integer(description='The playlist ID to which the slide belongs'), + 'enabled': fields.Boolean(description='Is the slide enabled?'), + 'delegate_duration': fields.Boolean(description='Should the duration be delegated?'), + 'duration': fields.Integer(description='Duration of the slide'), + 'position': fields.Integer(description='Position of the slide'), + 'is_notification': fields.Boolean(description='Is the slide a notification?'), + 'cron_schedule': fields.String(description='Cron expression for scheduling start'), + 'cron_schedule_end': fields.String(description='Cron expression for scheduling end'), +}) + +# Input model for updating slide positions +positions_model = slide_ns.model('SlidePositions', { + 'positions': fields.Raw(required=True, description='A dictionary where keys are slide IDs and values are their new positions') +}) class SlideApiController(ObController): def register(self): - self._app.add_url_rule('/api/slide', 'api_slide_add', self.add_slide, methods=['POST']) - self._app.add_url_rule('/api/slide/notification', 'api_slide_notification_add', self.add_notification, methods=['POST']) - self._app.add_url_rule('/api/slide/', 'api_slide_get', self.get_slide, methods=['GET']) - self._app.add_url_rule('/api/slide/', 'api_slide_edit', self.edit_slide, methods=['PUT']) - self._app.add_url_rule('/api/slide/', 'api_slide_delete', self.delete_slide, methods=['DELETE']) - self._app.add_url_rule('/api/slide/positions', 'api_slide_positions', self.update_slide_positions, methods=['POST']) + self.api().add_namespace(slide_ns, path='/api/slides') + slide_ns.add_resource(self.create_resource(SlideResource), '/') + slide_ns.add_resource(self.create_resource(SlideAddResource), '/') + slide_ns.add_resource(self.create_resource(SlideAddNotificationResource), '/notifications') + slide_ns.add_resource(self.create_resource(SlidePositionResource), '/positions') - def get_slide(self, slide_id: int): - slide = self._model_store.slide().get(slide_id) - if not slide: - abort(404, description="Slide not found") - return jsonify(slide.to_dict()) - - def add_slide(self): - return self.add_slide_or_notification(is_notification=False) - - def add_notification(self): - return self.add_slide_or_notification(is_notification=True) - - def add_slide_or_notification(self, is_notification=False): - data = request.get_json() + def create_resource(self, resource_class): + # Function to inject dependencies into resources + return type(f'{resource_class.__name__}WithDependencies', (resource_class,), { + '_model_store': self._model_store, + '_controller': self + }) + def _add_slide_or_notification(self, data, is_notification=False): if not data or 'content_id' not in data: abort(400, description="Valid Content ID is required") @@ -67,56 +93,7 @@ class SlideApiController(ObController): slide = self._model_store.slide().add_form(slide) self._post_update() - return jsonify(slide.to_dict()), 201 - - def edit_slide(self, slide_id: int): - data = request.get_json() - if not data or 'content_id' not in data: - abort(400, description="Content ID is required") - - slide = self._model_store.slide().get(slide_id) - if not slide: - abort(404, description="Slide not found") - - cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=slide.is_notification) - - self._model_store.slide().update_form( - id=slide_id, - content_id=data.get('content_id', slide.content_id), - enabled=data.get('enabled', slide.enabled), - position=data.get('position', slide.position), - delegate_duration=data.get('delegate_duration', slide.delegate_duration), - duration=data.get('duration', slide.duration), - cron_schedule=cron_schedule_start if 'scheduling' in data else slide.cron_schedule, - cron_schedule_end=cron_schedule_end if 'scheduling' in data else slide.cron_schedule_end, - ) - self._post_update() - - updated_slide = self._model_store.slide().get(slide_id) - return jsonify(updated_slide.to_dict()) - - def delete_slide(self, slide_id: int): - slide = self._model_store.slide().get(slide_id) - - if not slide: - abort(404, description="Slide not found") - - self._model_store.slide().delete(slide_id) - self._post_update() - - return '', 204 - - def update_slide_positions(self): - data = request.get_json() - if not data: - abort(400, description="Positions data are required") - - self._model_store.slide().update_positions(data) - self._post_update() - return jsonify({'status': 'ok'}) - - def _post_update(self): - self._model_store.variable().update_by_name("last_slide_update", time.time()) + return slide.to_dict(), 201 def _resolve_scheduling(self, data, is_notification=False): try: @@ -182,3 +159,100 @@ class SlideApiController(ObController): abort(400, description="Invalid value for notification scheduling. Expected 'datetime' or 'cron'.") return cron_schedule_start, cron_schedule_end + + def _post_update(self): + self._model_store.variable().update_by_name("last_slide_update", time.time()) + + +class SlideAddResource(Resource): + + @slide_ns.expect(slide_model) + @slide_ns.marshal_with(slide_output_model, code=201) + def post(self): + """Add a new slide""" + data = request.get_json() + return self._controller._add_slide_or_notification(data, is_notification=False) + + +class SlideAddNotificationResource(Resource): + + @slide_ns.expect(slide_model) + @slide_ns.marshal_with(slide_output_model, code=201) + def post(self): + """Add a new slide""" + data = request.get_json() + return self._controller._add_slide_or_notification(data, is_notification=True) + + +class SlideResource(Resource): + + @slide_ns.marshal_with(slide_output_model) + def get(self, slide_id): + """Get a slide by its ID""" + slide = self._model_store.slide().get(slide_id) + if not slide: + abort(404, description="Slide not found") + return slide.to_dict() + + @slide_ns.expect(slide_model) + @slide_ns.marshal_with(slide_output_model) + def put(self, slide_id): + """Edit an existing slide""" + data = request.get_json() + + slide = self._model_store.slide().get(slide_id) + if not slide: + abort(404, description="Slide not found") + + cron_schedule_start = slide.cron_schedule + cron_schedule_end = slide.cron_schedule_end + + if 'scheduling' in data: + cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification) + + self._model_store.slide().update_form( + id=slide_id, + content_id=data.get('content_id', slide.content_id), + enabled=data.get('enabled', slide.enabled), + position=data.get('position', slide.position), + delegate_duration=data.get('delegate_duration', slide.delegate_duration), + duration=data.get('duration', slide.duration), + cron_schedule=cron_schedule_start, + cron_schedule_end=cron_schedule_end + ) + self._controller._post_update() + + updated_slide = self._model_store.slide().get(slide_id) + return updated_slide.to_dict() + + def delete(self, slide_id): + """Delete a slide""" + slide = self._model_store.slide().get(slide_id) + + if not slide: + abort(404, description="Slide not found") + + self._model_store.slide().delete(slide_id) + self._controller._post_update() + + return '', 204 + + +class SlidePositionResource(Resource): + + @slide_ns.expect(positions_model) + def post(self): + """Update positions of multiple slides""" + data = request.get_json() + positions = data.get('positions', None) if data else None + + if not positions: + abort(400, description="Positions data are required") + + # Ensure the input is a dictionary with integer keys and values + if not isinstance(data, dict) or not all(isinstance(k, str) and isinstance(v, int) for k, v in positions.items()): + abort(400, description="Input must be a dictionary with string keys as slide IDs and integer values as positions") + + self._model_store.slide().update_positions(positions) + self._controller._post_update() + return jsonify({'status': 'ok'}) diff --git a/requirements.txt b/requirements.txt index e1c1025..545f5eb 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ waitress flask-login pysqlite3 psutil +flask-restx diff --git a/src/interface/ObController.py b/src/interface/ObController.py index 6ee9de1..2842e02 100644 --- a/src/interface/ObController.py +++ b/src/interface/ObController.py @@ -48,3 +48,6 @@ class ObController(abc.ABC): def render_view(self, template_file: str, **parameters: dict) -> str: return self._template_renderer.render_view(template_file, self.plugin(), **parameters) + + def api(self): + return self._web_server.api diff --git a/src/manager/ConfigManager.py b/src/manager/ConfigManager.py index b45b361..25b3bc3 100644 --- a/src/manager/ConfigManager.py +++ b/src/manager/ConfigManager.py @@ -9,6 +9,7 @@ load_dotenv() class ConfigManager: + APPLICATION_NAME = "Obscreen" DEFAULT_PORT = 5000 DEFAULT_PORT_HTTP_EXTERNAL_STORAGE = 5001 VERSION_FILE = 'version.txt' @@ -16,6 +17,7 @@ class ConfigManager: def __init__(self, replacers: Dict): self._replacers = replacers self._CONFIG = { + 'application_name': self.APPLICATION_NAME, 'version': None, 'demo': False, 'port_http_external_storage': self.DEFAULT_PORT_HTTP_EXTERNAL_STORAGE, @@ -87,7 +89,7 @@ class ConfigManager: if args.log_stdout: self._CONFIG['log_stdout'] = args.log_stdout if args.version: - print("Obscreen version v{} (https://github.com/jr-k/obscreen)".format(self._CONFIG['version'])) + print("{} version v{} (https://github.com/jr-k/obscreen)".format(self.APPLICATION_NAME, self._CONFIG['version'])) sys.exit(0) def load_from_env(self) -> None: diff --git a/src/manager/PlaylistManager.py b/src/manager/PlaylistManager.py index fefb219..ef6d770 100644 --- a/src/manager/PlaylistManager.py +++ b/src/manager/PlaylistManager.py @@ -165,7 +165,7 @@ GROUP BY playlist_id; return form = { - "name": name, + "name": name if isinstance(name, str) else slide.name, "time_sync": time_sync if isinstance(time_sync, bool) else slide.time_sync, "enabled": enabled if isinstance(enabled, bool) else slide.enabled, } diff --git a/src/service/WebServer.py b/src/service/WebServer.py index eb79cc9..c6a732e 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -4,6 +4,7 @@ from waitress import serve from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response from flask_login import LoginManager, current_user +from flask_restx import Api from src.manager.UserManager import UserManager from src.service.ModelStore import ModelStore @@ -27,6 +28,7 @@ class WebServer: def __init__(self, kernel, model_store: ModelStore, template_renderer: TemplateRenderer): self._app = None + self._api = None self._auth_enabled = False self._login_manager = None self._kernel = kernel @@ -35,6 +37,10 @@ class WebServer: self._debug = self._model_store.config().map().get('debug') self.setup() + @property + def api(self) -> Api: + return self._api + def run(self) -> None: serve( self._app, @@ -51,6 +57,7 @@ class WebServer: self._setup_web_globals() self._setup_web_errors() self._setup_web_controllers() + self._setup_api() def get_app(self): return self._app @@ -76,6 +83,7 @@ class WebServer: self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS) self._app.config['MAX_CONTENT_LENGTH'] = self._model_store.variable().map().get('slide_upload_limit').as_int() * 1024 * 1024 + self._app.config['ERROR_404_HELP'] = False self._setup_flask_login() @@ -115,6 +123,16 @@ class WebServer: PlaylistController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer) AuthController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer) + def _setup_api(self) -> None: + self._api = Api( + self._app, + version=self._model_store.config().map().get('version'), + title="{} {}".format(self._model_store.config().map().get('application_name'), "API"), + description='API Documentation with Swagger', + endpoint='api', + doc='/api' + ) + def _setup_web_globals(self) -> None: @self._app.context_processor def inject_global_vars() -> dict: From fbb194e44cf39d678b93adef33ab761860a9e502 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 05:03:46 +0200 Subject: [PATCH 4/9] flask ok --- .../controller/ContentApiController.py | 197 ++++++++++-------- .../controller/PlaylistApiController.py | 2 +- .../exception/ContentNotFoundException.py | 9 +- .../exception/ContentPathMissingException.py | 9 +- .../exception/FolderNotEmptyException.py | 9 +- .../exception/FolderNotFoundException.py | 9 +- src/manager/ContentManager.py | 2 +- src/service/WebServer.py | 8 +- 8 files changed, 121 insertions(+), 124 deletions(-) diff --git a/plugins/system/CoreApi/controller/ContentApiController.py b/plugins/system/CoreApi/controller/ContentApiController.py index c9335eb..952fbdc 100644 --- a/plugins/system/CoreApi/controller/ContentApiController.py +++ b/plugins/system/CoreApi/controller/ContentApiController.py @@ -1,5 +1,11 @@ +import os +import time +import logging + from flask import request, abort, jsonify -from flask_restx import Resource, Namespace, fields +from flask_restx import Resource, Namespace, fields, reqparse +from werkzeug.datastructures import FileStorage +from werkzeug.utils import secure_filename from src.model.entity.Content import Content from src.manager.FolderManager import FolderManager from src.model.enum.ContentType import ContentType @@ -13,15 +19,7 @@ from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEm # Namespace for content operations -content_ns = Namespace('content', description='Operations related to content management') - -# Input model for adding/updating content -content_model = content_ns.model('Content', { - 'name': fields.String(required=True, description='Name of the content'), - 'type': fields.String(required=True, description='Type of the content'), - 'location': fields.String(description='Location of the content (optional)'), - 'path': fields.String(description='Path of the folder') -}) +content_ns = Namespace('contents', description='Operations on contents') # Output model for content content_output_model = content_ns.model('ContentOutput', { @@ -35,31 +33,50 @@ content_output_model = content_ns.model('ContentOutput', { # Model for folder operations folder_model = content_ns.model('Folder', { 'name': fields.String(required=True, description='Name of the folder'), - 'path': fields.String(description='Path of the folder') + 'path': fields.String(required=False, description='Path context (with path starting with /)'), + 'folder_id': fields.Integer(required=False, description='Path context (with folder id)') }) # Model for bulk operations bulk_move_model = content_ns.model('BulkMove', { 'entity_ids': fields.List(fields.Integer, required=True, description='List of content IDs to move'), - 'path': fields.String(description='Path of the folder') + 'path': fields.String(required=True, description='Destination path for the content'), + 'folder_id': fields.Integer(required=False, description='Path context (with folder id)') }) +# Parser for content add/upload (single file) +content_upload_parser = content_ns.parser() +content_upload_parser.add_argument('name', type=str, required=True, help='Name of the content') +content_upload_parser.add_argument('type', type=str, required=True, help='Type of the content') +content_upload_parser.add_argument('object', type=FileStorage, location='files', required=True, help='File to be uploaded') +content_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)') +content_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)') + +# Parser for content add/bulk uploads (multiple files) +bulk_upload_parser = content_ns.parser() +bulk_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)') +bulk_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)') +bulk_upload_parser.add_argument('object', type=FileStorage, location='files', action='append', required=True, help='Files to be uploaded') + +# Parser for content edit +content_edit_parser = content_ns.parser() +content_edit_parser.add_argument('name', type=str, required=True, help='Name of the content') + +# Parser for content path context actions +path_parser = content_ns.parser() +path_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)') +path_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)') + +# Parser for folder add/edit +folder_parser = content_ns.parser() +folder_parser.add_argument('name', type=str, required=True, help='Name of the folder') +folder_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)') +folder_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)') class ContentApiController(ObController): def register(self): -# - self._app.add_url_rule('/api/content', 'api_content_list', self.list_content, methods=['GET']) -# - self._app.add_url_rule('/api/content', 'api_content_add', self.add_content, methods=['POST']) -# - self._app.add_url_rule('/api/content/', 'api_content_get', self.get_content, methods=['GET']) -# - self._app.add_url_rule('/api/content/', 'api_content_update', self.update_content, methods=['PUT']) -# - self._app.add_url_rule('/api/content/', 'api_content_delete', self.delete_content, methods=['DELETE']) -# - self._app.add_url_rule('/api/content/location/', 'api_content_location', self.location_content, methods=['GET']) -# - self._app.add_url_rule('/api/content/upload-bulk', 'api_content_upload_bulk', self.upload_bulk_content, methods=['POST']) -# - self._app.add_url_rule('/api/content/folder/move-bulk', 'api_folder_content_bulk_move', self.move_bulk_content_folder, methods=['POST']) -# - self._app.add_url_rule('/api/content/folder', 'api_folder_add', self.add_folder, methods=['POST']) -# - self._app.add_url_rule('/api/content/folder', 'api_folder_delete', self.delete_folder, methods=['DELETE']) -# - self._app.add_url_rule('/api/content/folder', 'api_folder_update', self.update_folder, methods=['PUT']) - self.api().add_namespace(content_ns, path='/api/content') + self.api().add_namespace(content_ns, path='/api/contents') content_ns.add_resource(self.create_resource(ContentListResource), '/') content_ns.add_resource(self.create_resource(ContentResource), '/') content_ns.add_resource(self.create_resource(ContentLocationResource), '/location/') @@ -74,31 +91,22 @@ class ContentApiController(ObController): '_controller': self }) - def get_request_data(self): - data = {} + def _get_folder_context(self, data): + path = data.get('path', None) + folder_id = data.get('folder_id', None) - try: - if 'multipart/form-data' in request.headers.get('Content-Type'): - data = request.form - else: - data = request.get_json() - except: - pass + if folder_id: + folder = self._model_store.folder().get(id=folder_id) - return data - - def get_folder_context(self): - data = self.get_request_data() - path = data.get('path') - path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if path and not path.startswith(FOLDER_ROOT_PATH) else path - - if 'folder_id' in data: - folder = self._model_store.folder().get(id=data.get('folder_id')) + if not folder: + raise FolderNotFoundException() return path, folder - if 'path' not in data: + if not path: raise ContentPathMissingException() + path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if not path.startswith(FOLDER_ROOT_PATH) else path + folder = self._model_store.folder().get_one_by_path(path=path, entity=FolderEntity.CONTENT) is_root_drive = FolderManager.is_root_drive(path) @@ -107,20 +115,26 @@ class ContentApiController(ObController): return FOLDER_ROOT_PATH if is_root_drive else path, folder + def _post_update(self): + self._model_store.variable().update_by_name("last_content_update", time.time()) + class ContentListResource(Resource): + @content_ns.expect(path_parser) @content_ns.marshal_list_with(content_output_model) def get(self): """List all contents""" - data = self._controller.get_request_data() + data = path_parser.parse_args() working_folder_path = None working_folder = None folder_id = None try: - working_folder_path, working_folder = self._controller.get_folder_context() + working_folder_path, working_folder = self._controller._get_folder_context(data) folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id) + except FolderNotFoundException: + pass except ContentPathMissingException: pass @@ -130,37 +144,31 @@ class ContentListResource(Resource): ) result = [content.to_dict() for content in contents] - return { - 'contents': result, - 'working_folder_path': working_folder_path, - 'working_folder': working_folder.to_dict() if working_folder else None - } + return result - @content_ns.expect(content_model) + @content_ns.expect(content_upload_parser) @content_ns.marshal_with(content_output_model, code=201) def post(self): """Add new content""" - data = self._controller.get_request_data() - working_folder_path, working_folder = self._controller.get_folder_context() - - if 'name' not in data: - abort(400, description="Name is required") - - if 'type' not in data: - abort(400, description="Type is required") - - if data.get('type') not in {type.value for type in ContentType}: - abort(400, description="Invalid type") + data = content_upload_parser.parse_args() + working_folder_path, working_folder = self._controller._get_folder_context(data) content_type = str_to_enum(data.get('type'), ContentType) - location = data.get('location', None) + + # Handle file upload + file = data.get('object') + if not file: + abort(400, description="File is required") + + filename = secure_filename(file.filename) + file.save(os.path.join(self._controller._app.config['UPLOAD_FOLDER'], filename)) content = self._model_store.content().add_form_raw( name=data.get('name'), type=content_type, - request_files=request.files, + request_files=file, upload_dir=self._controller._app.config['UPLOAD_FOLDER'], - location=location, + location=None, folder_id=working_folder.id if working_folder else None ) @@ -181,11 +189,11 @@ class ContentResource(Resource): return content.to_dict() - @content_ns.expect(content_model) + @content_ns.expect(content_edit_parser) @content_ns.marshal_with(content_output_model) def put(self, content_id: int): """Update existing content""" - data = self._controller.get_request_data() + data = content_edit_parser.parse_args() content = self._model_store.content().get(content_id) if not content: @@ -235,24 +243,27 @@ class ContentLocationResource(Resource): class ContentBulkUploadResource(Resource): + @content_ns.expect(bulk_upload_parser) def post(self): """Upload multiple content files""" - working_folder_path, working_folder = self._controller.get_folder_context() + data = bulk_upload_parser.parse_args() + working_folder_path, working_folder = self._controller._get_folder_context(data) - for key in request.files: - files = request.files.getlist(key) - for file in files: - content_type = ContentType.guess_content_type_file(file.filename) - name = file.filename.rsplit('.', 1)[0] + for file in data.get('object'): + content_type = ContentType.guess_content_type_file(file.filename) + name = file.filename.rsplit('.', 1)[0] - if content_type: - self._model_store.content().add_form_raw( - name=name, - type=content_type, - request_files=file, - upload_dir=self._controller._app.config['UPLOAD_FOLDER'], - folder_id=working_folder.id if working_folder else None - ) + if content_type: + filename = secure_filename(file.filename) + file.save(os.path.join(self._controller._app.config['UPLOAD_FOLDER'], filename)) + + self._model_store.content().add_form_raw( + name=name, + type=content_type, + request_files=file, + upload_dir=self._controller._app.config['UPLOAD_FOLDER'], + folder_id=working_folder.id if working_folder else None + ) return {'status': 'ok'}, 201 @@ -262,8 +273,9 @@ class FolderBulkMoveResource(Resource): @content_ns.expect(bulk_move_model) def post(self): """Move multiple content to another folder""" - data = self._controller.get_request_data() - working_folder_path, working_folder = self._controller.get_folder_context() + data = request.form + + working_folder_path, working_folder = self._controller._get_folder_context(data) if 'entity_ids' not in data: abort(400, description="Content IDs are required under 'entity_ids' field") @@ -273,7 +285,7 @@ class FolderBulkMoveResource(Resource): for entity_id in entity_ids: self._model_store.folder().move_to_folder( entity_id=entity_id, - folder_id=working_folder if working_folder else None, + folder_id=working_folder.id if working_folder else None, entity_is_folder=False, entity=FolderEntity.CONTENT ) @@ -283,12 +295,12 @@ class FolderBulkMoveResource(Resource): class FolderResource(Resource): - @content_ns.expect(folder_model) + @content_ns.expect(folder_parser) @content_ns.marshal_with(folder_model, code=201) def post(self): """Add a new folder""" - data = self._controller.get_request_data() - working_folder_path, working_folder = self._controller.get_folder_context() + data = folder_parser.parse_args() + working_folder_path, working_folder = self._controller._get_folder_context(data) if 'name' not in data: abort(400, description="Name is required") @@ -301,9 +313,11 @@ class FolderResource(Resource): return folder.to_dict(), 201 + @content_ns.expect(path_parser) def delete(self): """Delete a folder""" - working_folder_path, working_folder = self._controller.get_folder_context() + data = path_parser.parse_args() + working_folder_path, working_folder = self._controller._get_folder_context(data) if not working_folder: abort(400, description="You can't delete this folder") @@ -319,11 +333,11 @@ class FolderResource(Resource): return {'status': 'ok'}, 204 - @content_ns.expect(folder_model) + @content_ns.expect(folder_parser) def put(self): """Update a folder""" - data = self._controller.get_request_data() - working_folder_path, working_folder = self._controller.get_folder_context() + data = folder_parser.parse_args() + working_folder_path, working_folder = self._controller._get_folder_context(data) if 'name' not in data: abort(400, description="Name is required") @@ -337,4 +351,3 @@ class FolderResource(Resource): ) return {'status': 'ok'} - diff --git a/plugins/system/CoreApi/controller/PlaylistApiController.py b/plugins/system/CoreApi/controller/PlaylistApiController.py index b586952..32c82b6 100644 --- a/plugins/system/CoreApi/controller/PlaylistApiController.py +++ b/plugins/system/CoreApi/controller/PlaylistApiController.py @@ -4,7 +4,7 @@ from src.model.entity.Playlist import Playlist from src.interface.ObController import ObController # Namespace pour les opérations sur les playlists -playlist_ns = Namespace('playlists', description='Playlist operations') +playlist_ns = Namespace('playlists', description='Operations on playlist') # Modèle d'entrée pour la playlist playlist_model = playlist_ns.model('Playlist', { diff --git a/plugins/system/CoreApi/exception/ContentNotFoundException.py b/plugins/system/CoreApi/exception/ContentNotFoundException.py index 5217fc0..c9afcc9 100644 --- a/plugins/system/CoreApi/exception/ContentNotFoundException.py +++ b/plugins/system/CoreApi/exception/ContentNotFoundException.py @@ -2,10 +2,5 @@ from src.exceptions.HttpClientException import HttpClientException class ContentNotFoundException(HttpClientException): - @property - def code(self) -> int: - return 404 - - @property - def description(self) -> int: - return "Content not found" + code = 404 + description = "Content not found" diff --git a/plugins/system/CoreApi/exception/ContentPathMissingException.py b/plugins/system/CoreApi/exception/ContentPathMissingException.py index bc48c61..fb1ab33 100644 --- a/plugins/system/CoreApi/exception/ContentPathMissingException.py +++ b/plugins/system/CoreApi/exception/ContentPathMissingException.py @@ -2,10 +2,5 @@ from src.exceptions.HttpClientException import HttpClientException class ContentPathMissingException(HttpClientException): - @property - def code(self) -> int: - return 400 - - @property - def description(self) -> int: - return "Valid path is required" + code = 400 + description = "Path is required" diff --git a/plugins/system/CoreApi/exception/FolderNotEmptyException.py b/plugins/system/CoreApi/exception/FolderNotEmptyException.py index 243523a..a2ad8bf 100644 --- a/plugins/system/CoreApi/exception/FolderNotEmptyException.py +++ b/plugins/system/CoreApi/exception/FolderNotEmptyException.py @@ -2,10 +2,5 @@ from src.exceptions.HttpClientException import HttpClientException class FolderNotEmptyException(HttpClientException): - @property - def code(self) -> int: - return 400 - - @property - def description(self) -> int: - return "Folder is not empty" + code = 400 + description = "Folder is not empty" diff --git a/plugins/system/CoreApi/exception/FolderNotFoundException.py b/plugins/system/CoreApi/exception/FolderNotFoundException.py index c2a33e7..aa1c29b 100644 --- a/plugins/system/CoreApi/exception/FolderNotFoundException.py +++ b/plugins/system/CoreApi/exception/FolderNotFoundException.py @@ -2,10 +2,5 @@ from src.exceptions.HttpClientException import HttpClientException class FolderNotFoundException(HttpClientException): - @property - def code(self) -> int: - return 404 - - @property - def description(self) -> int: - return "Folder not found" + code = 404 + description = "Folder not found" diff --git a/src/manager/ContentManager.py b/src/manager/ContentManager.py index c43eb97..4a23ff2 100644 --- a/src/manager/ContentManager.py +++ b/src/manager/ContentManager.py @@ -98,7 +98,7 @@ class ContentManager(ModelManager): if slide_id: query = "{} {}".format(query, "AND slide_id = {}".format(slide_id)) - if isinstance(folder_id, int): + if folder_id is not None: if folder_id == 0: query = "{} {}".format(query, "AND folder_id is null") else: diff --git a/src/service/WebServer.py b/src/service/WebServer.py index c6a732e..ee309b2 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -41,12 +41,16 @@ class WebServer: def api(self) -> Api: return self._api + def get_max_upload_size(self): + return self._model_store.variable().map().get('slide_upload_limit').as_int() * 1024 * 1024 + def run(self) -> None: serve( self._app, host=self._model_store.config().map().get('bind'), port=self._model_store.config().map().get('port'), - threads=100 + threads=100, + max_request_body_size=self.get_max_upload_size(), ) def reload(self) -> None: @@ -82,7 +86,7 @@ class WebServer: ) self._app.config['UPLOAD_FOLDER'] = "{}/{}".format(WebDirConstant.FOLDER_STATIC, WebDirConstant.FOLDER_STATIC_WEB_UPLOADS) - self._app.config['MAX_CONTENT_LENGTH'] = self._model_store.variable().map().get('slide_upload_limit').as_int() * 1024 * 1024 + self._app.config['MAX_CONTENT_LENGTH'] = self.get_max_upload_size() self._app.config['ERROR_404_HELP'] = False self._setup_flask_login() From 443650b217169d42f56d2cc136744dbcad7bc7c0 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 17:18:14 +0200 Subject: [PATCH 5/9] full api with flask-restx/swagger --- .../controller/ContentApiController.py | 64 +++++--- .../controller/PlaylistApiController.py | 52 ++++-- .../CoreApi/controller/SlideApiController.py | 152 +++++++++++++----- src/manager/ContentManager.py | 2 +- src/manager/DatabaseManager.py | 2 + src/manager/PlaylistManager.py | 6 +- src/manager/SlideManager.py | 4 +- src/manager/UserManager.py | 18 ++- src/model/entity/User.py | 17 +- src/service/PluginStore.py | 4 + src/service/WebServer.py | 54 ++++++- src/util/utils.py | 11 ++ 12 files changed, 291 insertions(+), 95 deletions(-) diff --git a/plugins/system/CoreApi/controller/ContentApiController.py b/plugins/system/CoreApi/controller/ContentApiController.py index 952fbdc..921071c 100644 --- a/plugins/system/CoreApi/controller/ContentApiController.py +++ b/plugins/system/CoreApi/controller/ContentApiController.py @@ -8,7 +8,7 @@ from werkzeug.datastructures import FileStorage from werkzeug.utils import secure_filename from src.model.entity.Content import Content from src.manager.FolderManager import FolderManager -from src.model.enum.ContentType import ContentType +from src.model.enum.ContentType import ContentType, ContentInputType from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH from src.interface.ObController import ObController from src.util.utils import str_to_enum @@ -16,6 +16,7 @@ from plugins.system.CoreApi.exception.ContentPathMissingException import Content from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException +from src.service.WebServer import create_require_api_key_decorator # Namespace for content operations @@ -37,20 +38,27 @@ folder_model = content_ns.model('Folder', { 'folder_id': fields.Integer(required=False, description='Path context (with folder id)') }) -# Model for bulk operations -bulk_move_model = content_ns.model('BulkMove', { - 'entity_ids': fields.List(fields.Integer, required=True, description='List of content IDs to move'), - 'path': fields.String(required=True, description='Destination path for the content'), - 'folder_id': fields.Integer(required=False, description='Path context (with folder id)') -}) +# Parser for bulk move operations +bulk_move_parser = content_ns.parser() +bulk_move_parser.add_argument('entity_ids', type=int, action='append', required=True, help='List of content IDs to move') +bulk_move_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)') +bulk_move_parser.add_argument('folder_id', type=int, required=False, help='Path context (with folder id)') # Parser for content add/upload (single file) content_upload_parser = content_ns.parser() content_upload_parser.add_argument('name', type=str, required=True, help='Name of the content') content_upload_parser.add_argument('type', type=str, required=True, help='Type of the content') -content_upload_parser.add_argument('object', type=FileStorage, location='files', required=True, help='File to be uploaded') content_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)') content_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)') +content_upload_parser.add_argument('location', type=str, required=False, help="Content location (valid for types: {}, {} and {})".format( + ContentType.URL.value, + ContentType.YOUTUBE.value, + ContentType.EXTERNAL_STORAGE.value +)) +content_upload_parser.add_argument('object', type=FileStorage, location='files', required=False, help="Content location (valid for types: {} and {})".format( + ContentType.PICTURE.value, + ContentType.VIDEO.value +)) # Parser for content add/bulk uploads (multiple files) bulk_upload_parser = content_ns.parser() @@ -88,7 +96,8 @@ class ContentApiController(ObController): # Function to inject dependencies into resources return type(f'{resource_class.__name__}WithDependencies', (resource_class,), { '_model_store': self._model_store, - '_controller': self + '_controller': self, + 'require_api_key': create_require_api_key_decorator(self._web_server) }) def _get_folder_context(self, data): @@ -125,6 +134,7 @@ class ContentListResource(Resource): @content_ns.marshal_list_with(content_output_model) def get(self): """List all contents""" + self.require_api_key() data = path_parser.parse_args() working_folder_path = None working_folder = None @@ -150,25 +160,31 @@ class ContentListResource(Resource): @content_ns.marshal_with(content_output_model, code=201) def post(self): """Add new content""" + self.require_api_key() data = content_upload_parser.parse_args() working_folder_path, working_folder = self._controller._get_folder_context(data) + location = data.get('location', None) + content_type = None - content_type = str_to_enum(data.get('type'), ContentType) + # Handle content type conversion + try: + content_type = str_to_enum(data.get('type'), ContentType) + except ValueError as e: + abort(400, description=str(e)) # Handle file upload - file = data.get('object') - if not file: - abort(400, description="File is required") + file = data.get('object', None) - filename = secure_filename(file.filename) - file.save(os.path.join(self._controller._app.config['UPLOAD_FOLDER'], filename)) + if ContentType.get_input(content_type) == ContentInputType.UPLOAD: + if not file: + abort(400, description="File is required") content = self._model_store.content().add_form_raw( name=data.get('name'), type=content_type, request_files=file, upload_dir=self._controller._app.config['UPLOAD_FOLDER'], - location=None, + location=location, folder_id=working_folder.id if working_folder else None ) @@ -183,6 +199,7 @@ class ContentResource(Resource): @content_ns.marshal_with(content_output_model) def get(self, content_id: int): """Get content by ID""" + self.require_api_key() content = self._model_store.content().get(content_id) if not content: raise ContentNotFoundException() @@ -193,6 +210,7 @@ class ContentResource(Resource): @content_ns.marshal_with(content_output_model) def put(self, content_id: int): """Update existing content""" + self.require_api_key() data = content_edit_parser.parse_args() content = self._model_store.content().get(content_id) @@ -213,6 +231,7 @@ class ContentResource(Resource): def delete(self, content_id: int): """Delete content""" + self.require_api_key() content = self._model_store.content().get(content_id) if not content: @@ -231,6 +250,7 @@ class ContentLocationResource(Resource): def get(self, content_id: int): """Get content location by ID""" + self.require_api_key() content = self._model_store.content().get(content_id) if not content: @@ -246,6 +266,7 @@ class ContentBulkUploadResource(Resource): @content_ns.expect(bulk_upload_parser) def post(self): """Upload multiple content files""" + self.require_api_key() data = bulk_upload_parser.parse_args() working_folder_path, working_folder = self._controller._get_folder_context(data) @@ -254,9 +275,6 @@ class ContentBulkUploadResource(Resource): name = file.filename.rsplit('.', 1)[0] if content_type: - filename = secure_filename(file.filename) - file.save(os.path.join(self._controller._app.config['UPLOAD_FOLDER'], filename)) - self._model_store.content().add_form_raw( name=name, type=content_type, @@ -270,10 +288,11 @@ class ContentBulkUploadResource(Resource): class FolderBulkMoveResource(Resource): - @content_ns.expect(bulk_move_model) + @content_ns.expect(bulk_move_parser) def post(self): """Move multiple content to another folder""" - data = request.form + self.require_api_key() + data = bulk_move_parser.parse_args() working_folder_path, working_folder = self._controller._get_folder_context(data) @@ -299,6 +318,7 @@ class FolderResource(Resource): @content_ns.marshal_with(folder_model, code=201) def post(self): """Add a new folder""" + self.require_api_key() data = folder_parser.parse_args() working_folder_path, working_folder = self._controller._get_folder_context(data) @@ -316,6 +336,7 @@ class FolderResource(Resource): @content_ns.expect(path_parser) def delete(self): """Delete a folder""" + self.require_api_key() data = path_parser.parse_args() working_folder_path, working_folder = self._controller._get_folder_context(data) @@ -336,6 +357,7 @@ class FolderResource(Resource): @content_ns.expect(folder_parser) def put(self): """Update a folder""" + self.require_api_key() data = folder_parser.parse_args() working_folder_path, working_folder = self._controller._get_folder_context(data) diff --git a/plugins/system/CoreApi/controller/PlaylistApiController.py b/plugins/system/CoreApi/controller/PlaylistApiController.py index 32c82b6..f4c3100 100644 --- a/plugins/system/CoreApi/controller/PlaylistApiController.py +++ b/plugins/system/CoreApi/controller/PlaylistApiController.py @@ -2,18 +2,14 @@ from flask import request, abort, jsonify from flask_restx import Resource, Namespace, fields from src.model.entity.Playlist import Playlist from src.interface.ObController import ObController +from src.util.utils import str_to_bool +from src.service.WebServer import create_require_api_key_decorator -# Namespace pour les opérations sur les playlists + +# Namespace for playlists operations playlist_ns = Namespace('playlists', description='Operations on playlist') -# Modèle d'entrée pour la playlist -playlist_model = playlist_ns.model('Playlist', { - 'name': fields.String(required=True, description='The playlist name'), - 'enabled': fields.Boolean(default=True, description='Is the playlist enabled?'), - 'time_sync': fields.Boolean(default=False, description='Is time synchronization enabled?') -}) - -# Modèle de sortie pour la playlist +# Output model for a playlist playlist_output_model = playlist_ns.model('PlaylistOutput', { 'id': fields.Integer(readOnly=True, description='The unique identifier of a playlist'), 'name': fields.String(required=True, description='The playlist name'), @@ -21,6 +17,16 @@ playlist_output_model = playlist_ns.model('PlaylistOutput', { 'time_sync': fields.Boolean(description='Is time synchronization enabled?') }) +# Parser for playlist attributes (add) +playlist_parser = playlist_ns.parser() +playlist_parser.add_argument('name', type=str, required=True, help='The playlist name') +playlist_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the playlist enabled?') +playlist_parser.add_argument('time_sync', type=str_to_bool, default=None, help='Is time synchronization enabled for slideshow?') + +# Parser for playlist attributes (update) +playlist_edit_parser = playlist_parser.copy() +playlist_edit_parser.replace_argument('name', type=str, required=False, help='The playlist name') + class PlaylistApiController(ObController): @@ -34,30 +40,36 @@ class PlaylistApiController(ObController): def create_resource(self, resource_class): # Function to inject dependencies into resources return type(f'{resource_class.__name__}WithDependencies', (resource_class,), { - '_model_store': self._model_store + '_model_store': self._model_store, + '_controller': self, + 'require_api_key': create_require_api_key_decorator(self._web_server) }) class PlaylistListResource(Resource): + @playlist_ns.marshal_list_with(playlist_output_model) def get(self): """List all playlists""" + self.require_api_key() playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True) result = [playlist.to_dict() for playlist in playlists] return result - @playlist_ns.expect(playlist_model) + @playlist_ns.expect(playlist_parser) @playlist_ns.marshal_with(playlist_output_model, code=201) def post(self): """Create a new playlist""" - data = request.get_json() - if not data or 'name' not in data: + self.require_api_key() + data = playlist_parser.parse_args() + + if not data.get('name'): abort(400, description="Invalid input") playlist = Playlist( name=data.get('name'), - enabled=data.get('enabled', True), - time_sync=data.get('time_sync', False) + enabled=data.get('enabled') if data.get('enabled') is not None else True, + time_sync=data.get('time_sync') if data.get('time_sync') is not None else False, ) try: @@ -73,18 +85,21 @@ class PlaylistResource(Resource): @playlist_ns.marshal_with(playlist_output_model) def get(self, playlist_id): """Get a playlist by its ID""" + self.require_api_key() playlist = self._model_store.playlist().get(playlist_id) if not playlist: abort(404, description="Playlist not found") return playlist.to_dict() - @playlist_ns.expect(playlist_model) + @playlist_ns.expect(playlist_edit_parser) @playlist_ns.marshal_with(playlist_output_model) def put(self, playlist_id): """Update an existing playlist""" - data = request.get_json() + self.require_api_key() + data = playlist_edit_parser.parse_args() playlist = self._model_store.playlist().get(playlist_id) + if not playlist: abort(404, description="Playlist not found") @@ -99,6 +114,7 @@ class PlaylistResource(Resource): def delete(self, playlist_id): """Delete a playlist""" + self.require_api_key() playlist = self._model_store.playlist().get(playlist_id) if not playlist: abort(404, description="Playlist not found") @@ -117,6 +133,7 @@ class PlaylistSlidesResource(Resource): def get(self, playlist_id): """Get slides associated with a playlist""" + self.require_api_key() playlist = self._model_store.playlist().get(playlist_id) if not playlist: @@ -132,6 +149,7 @@ class PlaylistNotificationsResource(Resource): def get(self, playlist_id): """Get notifications associated with a playlist""" + self.require_api_key() playlist = self._model_store.playlist().get(playlist_id) if not playlist: diff --git a/plugins/system/CoreApi/controller/SlideApiController.py b/plugins/system/CoreApi/controller/SlideApiController.py index 605c889..feac1f7 100644 --- a/plugins/system/CoreApi/controller/SlideApiController.py +++ b/plugins/system/CoreApi/controller/SlideApiController.py @@ -1,32 +1,15 @@ +import time + from flask import request, abort, jsonify from flask_restx import Resource, Namespace, fields from src.model.entity.Slide import Slide from src.interface.ObController import ObController -from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron -import time +from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron, str_to_bool +from src.service.WebServer import create_require_api_key_decorator # Namespace for slide operations slide_ns = Namespace('slides', description='Operations on slides') -# Input model for adding/editing a slide -slide_model = slide_ns.model('Slide', { - 'content_id': fields.Integer(required=True, description='The content ID for the slide'), - 'playlist_id': fields.Integer(required=True, description='The playlist ID to which the slide belongs'), - 'enabled': fields.Boolean(default=True, description='Is the slide enabled?'), - 'delegate_duration': fields.Boolean(default=False, description='Should the duration be delegated?'), - 'duration': fields.Integer(default=3, description='Duration of the slide'), - 'position': fields.Integer(default=999, description='Position of the slide'), - 'scheduling': fields.String(description='Scheduling type: loop, datetime, or inweek'), - 'datetime_start': fields.String(description='Start datetime for scheduling'), - 'datetime_end': fields.String(description='End datetime for scheduling'), - 'day_start': fields.Integer(description='Start day for inweek scheduling'), - 'time_start': fields.String(description='Start time for inweek scheduling'), - 'day_end': fields.Integer(description='End day for inweek scheduling'), - 'time_end': fields.String(description='End time for inweek scheduling'), - 'cron_start': fields.String(description='Cron expression for scheduling start'), - 'cron_end': fields.String(description='Cron expression for scheduling end'), -}) - # Output model for a slide slide_output_model = slide_ns.model('SlideOutput', { 'id': fields.Integer(readOnly=True, description='The unique identifier of a slide'), @@ -46,11 +29,51 @@ positions_model = slide_ns.model('SlidePositions', { 'positions': fields.Raw(required=True, description='A dictionary where keys are slide IDs and values are their new positions') }) +# Parser for basic slide attributes +slide_base_parser = slide_ns.parser() +slide_base_parser.add_argument('content_id', type=int, required=True, help='The content ID for the slide') +slide_base_parser.add_argument('playlist_id', type=int, required=True, help='The playlist ID to which the slide belongs') +slide_base_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the slide enabled?') +slide_base_parser.add_argument('duration', type=int, default=3, help='Duration of the slide') +slide_base_parser.add_argument('position', type=int, default=999, help='Position of the slide') + +# Parser for slide attributes (add) +slide_parser = slide_base_parser.copy() +slide_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: loop, datetime or inweek') +slide_parser.add_argument('delegate_duration', type=str_to_bool, default=None, help='Should the duration be delegated to video\'s duration?') +slide_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for scheduling (format: Y-m-d H:M)') +slide_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for scheduling (format: Y-m-d H:M)') +slide_parser.add_argument('day_start', type=int, required=False, help='Start day for inweek scheduling (format: 1 for Monday to 7 for Sunday)') +slide_parser.add_argument('time_start', type=str, required=False, help='Start time for inweek scheduling (format: H:M)') +slide_parser.add_argument('day_end', type=int, required=False, help='End day for inweek scheduling (format: 1 for Monday to 7 for Sunday)') +slide_parser.add_argument('time_end', type=str, required=False, help='End time for inweek scheduling (format: H:M)') + +# Parser for slide notification attributes (add) +slide_notification_parser = slide_base_parser.copy() +slide_notification_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: datetime or cron') +slide_notification_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for notification scheduling (format: Y-m-d H:M)') +slide_notification_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for notification scheduling (format: Y-m-d H:M)') +slide_notification_parser.add_argument('cron_start', type=str, required=False, help='Cron expression for notification scheduling start (format: * * * * * * *)') +slide_notification_parser.add_argument('cron_end', type=str, required=False, help='Cron expression for notification scheduling end (format: * * * * * * *)') + +# Parser for slide attributes (update) +slide_edit_parser = slide_parser.copy() +slide_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: loop, datetime, or inweek') +slide_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide') +slide_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs') + +# Parser for slide notification attributes (update) +slide_notification_edit_parser = slide_notification_parser.copy() +slide_notification_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: datetime or cron') +slide_notification_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide') +slide_notification_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs') + class SlideApiController(ObController): def register(self): self.api().add_namespace(slide_ns, path='/api/slides') + slide_ns.add_resource(self.create_resource(SlideNotificationResource), '/notifications/') slide_ns.add_resource(self.create_resource(SlideResource), '/') slide_ns.add_resource(self.create_resource(SlideAddResource), '/') slide_ns.add_resource(self.create_resource(SlideAddNotificationResource), '/notifications') @@ -60,7 +83,8 @@ class SlideApiController(ObController): # Function to inject dependencies into resources return type(f'{resource_class.__name__}WithDependencies', (resource_class,), { '_model_store': self._model_store, - '_controller': self + '_controller': self, + 'require_api_key': create_require_api_key_decorator(self._web_server) }) def _add_slide_or_notification(self, data, is_notification=False): @@ -80,8 +104,8 @@ class SlideApiController(ObController): slide = Slide( content_id=data.get('content_id'), - enabled=data.get('enabled', True), - delegate_duration=data.get('delegate_duration', False), + enabled=data.get('enabled') if data.get('enabled') is not None else True, + delegate_duration=data.get('delegate_duration') if data.get('delegate_duration') is not None else False, duration=data.get('duration', 3), position=data.get('position', 999), is_notification=is_notification, @@ -166,21 +190,23 @@ class SlideApiController(ObController): class SlideAddResource(Resource): - @slide_ns.expect(slide_model) + @slide_ns.expect(slide_parser) @slide_ns.marshal_with(slide_output_model, code=201) def post(self): """Add a new slide""" - data = request.get_json() + self.require_api_key() + data = slide_parser.parse_args() return self._controller._add_slide_or_notification(data, is_notification=False) class SlideAddNotificationResource(Resource): - @slide_ns.expect(slide_model) + @slide_ns.expect(slide_notification_parser) @slide_ns.marshal_with(slide_output_model, code=201) def post(self): - """Add a new slide""" - data = request.get_json() + """Add a new slide notification""" + self.require_api_key() + data = slide_notification_parser.parse_args() return self._controller._add_slide_or_notification(data, is_notification=True) @@ -189,16 +215,18 @@ class SlideResource(Resource): @slide_ns.marshal_with(slide_output_model) def get(self, slide_id): """Get a slide by its ID""" + self.require_api_key() slide = self._model_store.slide().get(slide_id) if not slide: abort(404, description="Slide not found") return slide.to_dict() - @slide_ns.expect(slide_model) + @slide_ns.expect(slide_edit_parser) @slide_ns.marshal_with(slide_output_model) def put(self, slide_id): """Edit an existing slide""" - data = request.get_json() + self.require_api_key() + data = slide_edit_parser.parse_args() slide = self._model_store.slide().get(slide_id) if not slide: @@ -207,7 +235,54 @@ class SlideResource(Resource): cron_schedule_start = slide.cron_schedule cron_schedule_end = slide.cron_schedule_end - if 'scheduling' in data: + if data.get('scheduling'): + cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification) + + self._model_store.slide().update_form( + id=slide_id, + content_id=data.get('content_id', slide.content_id), + enabled=data.get('enabled', slide.enabled), + position=data.get('position', slide.position), + duration=data.get('duration', slide.duration), + cron_schedule=cron_schedule_start, + cron_schedule_end=cron_schedule_end + ) + self._controller._post_update() + + updated_slide = self._model_store.slide().get(slide_id) + return updated_slide.to_dict() + + def delete(self, slide_id): + """Delete a slide""" + self.require_api_key() + slide = self._model_store.slide().get(slide_id) + + if not slide: + abort(404, description="Slide not found") + + self._model_store.slide().delete(slide_id) + self._controller._post_update() + + return '', 204 + + +class SlideNotificationResource(Resource): + + @slide_ns.expect(slide_notification_edit_parser) + @slide_ns.marshal_with(slide_output_model) + def put(self, slide_id): + """Edit an existing slide notification""" + self.require_api_key() + data = slide_notification_edit_parser.parse_args() + + slide = self._model_store.slide().get(slide_id) + if not slide: + abort(404, description="Slide not found") + + cron_schedule_start = slide.cron_schedule + cron_schedule_end = slide.cron_schedule_end + + if data.get('scheduling'): cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification) self._model_store.slide().update_form( @@ -225,24 +300,13 @@ class SlideResource(Resource): updated_slide = self._model_store.slide().get(slide_id) return updated_slide.to_dict() - def delete(self, slide_id): - """Delete a slide""" - slide = self._model_store.slide().get(slide_id) - - if not slide: - abort(404, description="Slide not found") - - self._model_store.slide().delete(slide_id) - self._controller._post_update() - - return '', 204 - class SlidePositionResource(Resource): @slide_ns.expect(positions_model) def post(self): """Update positions of multiple slides""" + self.require_api_key() data = request.get_json() positions = data.get('positions', None) if data else None diff --git a/src/manager/ContentManager.py b/src/manager/ContentManager.py index 4a23ff2..aaf6c14 100644 --- a/src/manager/ContentManager.py +++ b/src/manager/ContentManager.py @@ -201,7 +201,7 @@ class ContentManager(ModelManager): content.duration = mp4_duration_with_ffprobe(content.location) else: - content.location = location + content.location = location if location else '' self.add_form(content) return self.get_one_by(query="uuid = '{}'".format(content.uuid)) diff --git a/src/manager/DatabaseManager.py b/src/manager/DatabaseManager.py index 999c59e..92e8d6f 100644 --- a/src/manager/DatabaseManager.py +++ b/src/manager/DatabaseManager.py @@ -3,6 +3,7 @@ import re import json import sqlite3 import logging +import uuid from sqlite3 import Cursor from typing import Optional, Dict @@ -216,6 +217,7 @@ class DatabaseManager: "UPDATE fleet_player_group SET slug = id WHERE slug = '' or slug is null", "UPDATE content SET uuid = id WHERE uuid = '' or uuid is null", "UPDATE slide SET uuid = id WHERE uuid = '' or uuid is null", + "UPDATE user SET apikey = \'{}\' || id WHERE apikey = '' or apikey is null".format(str(uuid.uuid4())), ] for query in queries: diff --git a/src/manager/PlaylistManager.py b/src/manager/PlaylistManager.py index ef6d770..5bc6e0f 100644 --- a/src/manager/PlaylistManager.py +++ b/src/manager/PlaylistManager.py @@ -165,9 +165,9 @@ GROUP BY playlist_id; return form = { - "name": name if isinstance(name, str) else slide.name, - "time_sync": time_sync if isinstance(time_sync, bool) else slide.time_sync, - "enabled": enabled if isinstance(enabled, bool) else slide.enabled, + "name": name if isinstance(name, str) else playlist.name, + "time_sync": time_sync if isinstance(time_sync, bool) else playlist.time_sync, + "enabled": enabled if isinstance(enabled, bool) else playlist.enabled, } self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form)) diff --git a/src/manager/SlideManager.py b/src/manager/SlideManager.py index 15eb17e..6703b04 100644 --- a/src/manager/SlideManager.py +++ b/src/manager/SlideManager.py @@ -144,8 +144,8 @@ class SlideManager(ModelManager): return form = { - "duration": duration if duration else slide.duration, - "content_id": content_id if content_id else slide.content_id, + "duration": duration if isinstance(duration, int) else slide.duration, + "content_id": content_id if isinstance(content_id, int) else slide.content_id, "position": position if isinstance(position, int) else slide.position, "enabled": enabled if isinstance(enabled, bool) else slide.enabled, "delegate_duration": delegate_duration if isinstance(delegate_duration, bool) else slide.delegate_duration, diff --git a/src/manager/UserManager.py b/src/manager/UserManager.py index df9473a..bb46b4b 100644 --- a/src/manager/UserManager.py +++ b/src/manager/UserManager.py @@ -15,6 +15,7 @@ class UserManager: TABLE_MODEL = [ "username CHAR(255)", "password CHAR(255)", + "apikey CHAR(255)", "enabled INTEGER DEFAULT 1", "created_by CHAR(255)", "updated_by CHAR(255)", @@ -79,8 +80,21 @@ class UserManager: return self.hydrate_object(object) - def get_one_by_username(self, username: str, enabled: bool = None) -> Optional[User]: - return self.get_one_by("username = '{}' and (enabled is null or enabled = {})".format(username, int(enabled))) + def get_one_by_username(self, username: str, enabled: Optional[bool] = None) -> Optional[User]: + query = " username = ? " + + if enabled: + query = "{} {}".format(query, "AND enabled = {}".format(int(enabled))) + + return self.get_one_by(query=query, values={"username": username}) + + def get_one_by_apikey(self, apikey: str, enabled: Optional[bool] = None) -> Optional[User]: + query = " apikey = ? " + + if enabled: + query = "{} {}".format(query, "AND enabled = {}".format(int(enabled))) + + return self.get_one_by(query=query, values={"apikey": apikey}) def count_all_enabled(self): return len(self.get_by("enabled = 1")) diff --git a/src/model/entity/User.py b/src/model/entity/User.py index af0caa8..2fdfccd 100644 --- a/src/model/entity/User.py +++ b/src/model/entity/User.py @@ -1,5 +1,6 @@ import json import time +import uuid from typing import Optional, Union @@ -8,16 +9,22 @@ class User: DEFAULT_USER = 'admin' - def __init__(self, username: str = '', password: str = '', enabled: bool = True, id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None): + def __init__(self, username: str = '', password: str = '', apikey: str = '', enabled: bool = True, id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None): self._id = id if id else None self._username = username self._password = password + self._apikey = apikey if apikey else self.set_new_apikey() self._enabled = enabled self._created_by = created_by if created_by else None self._updated_by = updated_by if updated_by else None self._created_at = int(created_at if created_at else time.time()) self._updated_at = int(updated_at if updated_at else time.time()) + def set_new_apikey(self) -> str: + self._apikey = str(uuid.uuid4()) + + return self._uuid + @property def id(self) -> Optional[int]: return self._id @@ -38,6 +45,14 @@ class User: def password(self, value: str): self._password = value + @property + def apikey(self) -> str: + return self._apikey + + @apikey.setter + def apikey(self, value: str): + self._apikey = value + @property def enabled(self) -> bool: return bool(self._enabled) diff --git a/src/service/PluginStore.py b/src/service/PluginStore.py index 067dc91..e435c37 100644 --- a/src/service/PluginStore.py +++ b/src/service/PluginStore.py @@ -3,6 +3,7 @@ import shutil import logging import inspect import importlib +from pathlib import Path from src.interface.ObPlugin import ObPlugin from src.interface.ObController import ObController @@ -55,6 +56,9 @@ class PluginStore: return self._hooks def find_plugins_in_directory(self, directory: str) -> list: + plugin_type = Path(directory).stem.capitalize() + logging.info("#") + logging.info("[plugin] {}...".format(plugin_type)) plugins = [] for root, dirs, files in os.walk('{}/{}'.format(self._kernel.get_application_dir(), directory)): for file in files: diff --git a/src/service/WebServer.py b/src/service/WebServer.py index ee309b2..3cef5e1 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -1,8 +1,9 @@ import os import time from waitress import serve +from functools import wraps -from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response +from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response, abort from flask_login import LoginManager, current_user from flask_restx import Api @@ -29,7 +30,6 @@ class WebServer: def __init__(self, kernel, model_store: ModelStore, template_renderer: TemplateRenderer): self._app = None self._api = None - self._auth_enabled = False self._login_manager = None self._kernel = kernel self._model_store = model_store @@ -104,9 +104,12 @@ class WebServer: def load_user(user_id): return self._model_store.user().get(user_id) + def is_auth_enabled(self) -> bool: + return self._model_store.variable().map().get('auth_enabled').as_bool() + def auth_required(self, f): def decorated_function(*args, **kwargs): - if not self._model_store.variable().map().get('auth_enabled').as_bool(): + if not self.is_auth_enabled(): return f(*args, **kwargs) if not current_user.is_authenticated: @@ -128,13 +131,28 @@ class WebServer: AuthController(self._kernel, self, self._app, self.auth_required, self._model_store, self._template_renderer) def _setup_api(self) -> None: + security = None + authorizations = None + + if self.is_auth_enabled(): + security = 'apikey' + authorizations = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization' + } + } + self._api = Api( self._app, version=self._model_store.config().map().get('version'), title="{} {}".format(self._model_store.config().map().get('application_name'), "API"), description='API Documentation with Swagger', endpoint='api', - doc='/api' + doc='/api', + security=security, + authorizations=authorizations ) def _setup_web_globals(self) -> None: @@ -163,3 +181,31 @@ class WebServer: self._app.register_error_handler(409, handle_error) self._app.register_error_handler(HttpClientException, handle_error) + +def create_require_api_key_decorator(web_server: WebServer): + def require_api_key(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not web_server.is_auth_enabled(): + return + + auth_header = request.headers.get('Authorization') + + if auth_header: + apikey = auth_header + parts = auth_header.split() + if parts[0].lower() == 'bearer': + apikey = parts[1] + + user = web_server._model_store.user().get_one_by_apikey(apikey) + + if user: + return user + + return abort(403, 'Forbidden: You do not have access to this resource.') + + return abort(401, 'Invalid or missing API key.') + + return decorated_function() + + return require_api_key diff --git a/src/util/utils.py b/src/util/utils.py index 59c9b5b..c4fce2d 100644 --- a/src/util/utils.py +++ b/src/util/utils.py @@ -325,3 +325,14 @@ def slugify_next(slug: str) -> str: return f"{parts[0]}-{next_number}" else: return f"{slug}-1" + + +def str_to_bool(value): + if isinstance(value, bool): + return value + if value.lower() in {'false', 'f', '0', 'no', 'n'}: + return False + elif value.lower() in {'true', 't', '1', 'yes', 'y'}: + return True + else: + raise ValueError('Boolean value expected.') From 1e5e9f0209f63ae38b466d770559575518278e15 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 17:38:30 +0200 Subject: [PATCH 6/9] token management ok --- data/www/css/compiled/main-dark-mode.css | 2 +- data/www/css/compiled/main-light-mode.css | 2 +- data/www/js/auth/users.js | 25 +++++++++++++++++++- data/www/scss/pages/_users.scss | 28 ++++++++++++++++++++++- src/model/entity/User.py | 2 +- src/service/WebServer.py | 3 ++- views/auth/component/table.jinja.html | 15 +++++++++++- 7 files changed, 70 insertions(+), 7 deletions(-) diff --git a/data/www/css/compiled/main-dark-mode.css b/data/www/css/compiled/main-dark-mode.css index a534c72..25cf5da 100644 --- a/data/www/css/compiled/main-dark-mode.css +++ b/data/www/css/compiled/main-dark-mode.css @@ -1 +1 @@ -.warning{color:#e56723!important}.bg-warning{background-color:#e56723!important}.border-warning{border-color:#e56723!important}.info{color:#027bff!important}.bg-info{background-color:#027bff!important}.border-info{border-color:#027bff!important}.info-alt{color:#075cb7!important}.bg-info-alt{background-color:#075cb7!important}.border-info-alt{border-color:#075cb7!important}.success{color:#0eef5f!important}.bg-success{background-color:#0eef5f!important}.border-success{border-color:#0eef5f!important}.success-alt{color:#11a948!important}.bg-success-alt{background-color:#11a948!important}.border-success-alt{border-color:#11a948!important}.error{color:#ef0e5d!important}.bg-error{background-color:#ef0e5d!important}.border-error{border-color:#ef0e5d!important}.error-alt{color:#c20941!important}.bg-error-alt{background-color:#c20941!important}.border-error-alt{border-color:#c20941!important}.danger{color:#ef0e5d!important}.bg-danger{background-color:#ef0e5d!important}.border-danger{border-color:#ef0e5d!important}.danger-alt{color:#c20941!important}.bg-danger-alt{background-color:#c20941!important}.border-danger-alt{border-color:#c20941!important}.purple{color:#bc48ff!important}.bg-purple{background-color:#bc48ff!important}.border-purple{border-color:#bc48ff!important}.purple-alt{color:#692fbd!important}.bg-purple-alt{background-color:#692fbd!important}.border-purple-alt{border-color:#692fbd!important}.neutral{color:#464646!important}.bg-neutral{background-color:#464646!important}.border-neutral{border-color:#464646!important}.yellow{color:#e5a123!important}.bg-yellow{background-color:#e5a123!important}.border-yellow{border-color:#e5a123!important}.white{color:#fff!important}.bg-white{background-color:#fff!important}.border-white{border-color:#fff!important}.black{color:#000!important}.bg-black{background-color:#000!important}.border-black{border-color:#000!important}.youtube{color:#fd3c01!important}.bg-youtube{background-color:#fd3c01!important}.border-youtube{border-color:#fd3c01!important}.raspbian{color:#b61240!important}.bg-raspbian{background-color:#b61240!important}.border-raspbian{border-color:#b61240!important}.windows{color:#12a7e3!important}.bg-windows{background-color:#12a7e3!important}.border-windows{border-color:#12a7e3!important}.macos{color:#b3bcc2!important}.bg-macos{background-color:#b3bcc2!important}.border-macos{border-color:#b3bcc2!important}.debian{color:#cf084e!important}.bg-debian{background-color:#cf084e!important}.border-debian{border-color:#cf084e!important}.fedora{color:#52a2da!important}.bg-fedora{background-color:#52a2da!important}.border-fedora{border-color:#52a2da!important}.ubuntu{color:#d64514!important}.bg-ubuntu{background-color:#d64514!important}.border-ubuntu{border-color:#d64514!important}.suse{color:#6fb425!important}.bg-suse{background-color:#6fb425!important}.border-suse{border-color:#6fb425!important}.redhat{color:#c60200!important}.bg-redhat{background-color:#c60200!important}.border-redhat{border-color:#c60200!important}.centos{color:#9b4c88!important}.bg-centos{background-color:#9b4c88!important}.border-centos{border-color:#9b4c88!important}.other{color:#e5a123!important}.bg-other{background-color:#e5a123!important}.border-other{border-color:#e5a123!important}button.btn-warning,.btn.btn-warning{background:#e56723;box-shadow:0 2px #913e11}button.btn-warning:hover,.btn.btn-warning:hover{box-shadow:0 2px 0 1px #913e11 inset}button.btn-warning:focus,.btn.btn-warning:focus{background:#913e11}button.btn-wire-warning,.btn.btn-wire-warning{background:transparent;box-shadow:none;border:2px solid rgb(229,103,35);color:#fffc}button.btn-wire-warning i.btn-match,.btn.btn-wire-warning i.btn-match{color:#e56723}button.btn-wire-warning:hover,.btn.btn-wire-warning:hover{background:#e567230d;border-color:#be5117;color:#be5117;box-shadow:none}button.btn-wire-warning:focus,.btn.btn-wire-warning:focus{border-color:#913e11;background:transparent}button.btn-info,.btn.btn-info{background:#027bff;box-shadow:0 2px #004a9b}button.btn-info:hover,.btn.btn-info:hover{box-shadow:0 2px 0 1px #004a9b inset}button.btn-info:focus,.btn.btn-info:focus{background:#004a9b}button.btn-wire-info,.btn.btn-wire-info{background:transparent;box-shadow:none;border:2px solid rgb(2,123,255);color:#fffc}button.btn-wire-info i.btn-match,.btn.btn-wire-info i.btn-match{color:#027bff}button.btn-wire-info:hover,.btn.btn-wire-info:hover{background:#027bff0d;border-color:#0063ce;color:#0063ce;box-shadow:none}button.btn-wire-info:focus,.btn.btn-wire-info:focus{border-color:#004a9b;background:transparent}button.btn-info-alt,.btn.btn-info-alt{background:#075cb7;box-shadow:0 2px #032b55}button.btn-info-alt:hover,.btn.btn-info-alt:hover{box-shadow:0 2px 0 1px #032b55 inset}button.btn-info-alt:focus,.btn.btn-info-alt:focus{background:#032b55}button.btn-wire-info-alt,.btn.btn-wire-info-alt{background:transparent;box-shadow:none;border:2px solid rgb(7,92,183);color:#fffc}button.btn-wire-info-alt i.btn-match,.btn.btn-wire-info-alt i.btn-match{color:#075cb7}button.btn-wire-info-alt:hover,.btn.btn-wire-info-alt:hover{background:#075cb70d;border-color:#054386;color:#054386;box-shadow:none}button.btn-wire-info-alt:focus,.btn.btn-wire-info-alt:focus{border-color:#032b55;background:transparent}button.btn-success,.btn.btn-success{background:#0eef5f;box-shadow:0 2px #088f39}button.btn-success:hover,.btn.btn-success:hover{box-shadow:0 2px 0 1px #088f39 inset}button.btn-success:focus,.btn.btn-success:focus{background:#088f39}button.btn-wire-success,.btn.btn-wire-success{background:transparent;box-shadow:none;border:2px solid rgb(14,239,95);color:#fffc}button.btn-wire-success i.btn-match,.btn.btn-wire-success i.btn-match{color:#0eef5f}button.btn-wire-success:hover,.btn.btn-wire-success:hover{background:#0eef5f0d;border-color:#0bbf4c;color:#0bbf4c;box-shadow:none}button.btn-wire-success:focus,.btn.btn-wire-success:focus{border-color:#088f39;background:transparent}button.btn-success-alt,.btn.btn-success-alt{background:#11a948;box-shadow:0 2px #084c21}button.btn-success-alt:hover,.btn.btn-success-alt:hover{box-shadow:0 2px 0 1px #084c21 inset}button.btn-success-alt:focus,.btn.btn-success-alt:focus{background:#084c21}button.btn-wire-success-alt,.btn.btn-wire-success-alt{background:transparent;box-shadow:none;border:2px solid rgb(17,169,72);color:#fffc}button.btn-wire-success-alt i.btn-match,.btn.btn-wire-success-alt i.btn-match{color:#11a948}button.btn-wire-success-alt:hover,.btn.btn-wire-success-alt:hover{background:#11a9480d;border-color:#0c7b34;color:#0c7b34;box-shadow:none}button.btn-wire-success-alt:focus,.btn.btn-wire-success-alt:focus{border-color:#084c21;background:transparent}button.btn-error,.btn.btn-error{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-error:hover,.btn.btn-error:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-error:focus,.btn.btn-error:focus{background:#8f0838}button.btn-wire-error,.btn.btn-wire-error{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#fffc}button.btn-wire-error i.btn-match,.btn.btn-wire-error i.btn-match{color:#ef0e5d}button.btn-wire-error:hover,.btn.btn-wire-error:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-error:focus,.btn.btn-wire-error:focus{border-color:#8f0838;background:transparent}button.btn-error-alt,.btn.btn-error-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-error-alt:hover,.btn.btn-error-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-error-alt:focus,.btn.btn-error-alt:focus{background:#610420}button.btn-wire-error-alt,.btn.btn-wire-error-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#fffc}button.btn-wire-error-alt i.btn-match,.btn.btn-wire-error-alt i.btn-match{color:#c20941}button.btn-wire-error-alt:hover,.btn.btn-wire-error-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-error-alt:focus,.btn.btn-wire-error-alt:focus{border-color:#610420;background:transparent}button.btn-danger,.btn.btn-danger{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-danger:hover,.btn.btn-danger:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-danger:focus,.btn.btn-danger:focus{background:#8f0838}button.btn-wire-danger,.btn.btn-wire-danger{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#fffc}button.btn-wire-danger i.btn-match,.btn.btn-wire-danger i.btn-match{color:#ef0e5d}button.btn-wire-danger:hover,.btn.btn-wire-danger:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-danger:focus,.btn.btn-wire-danger:focus{border-color:#8f0838;background:transparent}button.btn-danger-alt,.btn.btn-danger-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-danger-alt:hover,.btn.btn-danger-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-danger-alt:focus,.btn.btn-danger-alt:focus{background:#610420}button.btn-wire-danger-alt,.btn.btn-wire-danger-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#fffc}button.btn-wire-danger-alt i.btn-match,.btn.btn-wire-danger-alt i.btn-match{color:#c20941}button.btn-wire-danger-alt:hover,.btn.btn-wire-danger-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-danger-alt:focus,.btn.btn-wire-danger-alt:focus{border-color:#610420;background:transparent}button.btn-purple,.btn.btn-purple{background:#bc48ff;box-shadow:0 2px #8f00e1}button.btn-purple:hover,.btn.btn-purple:hover{box-shadow:0 2px 0 1px #8f00e1 inset}button.btn-purple:focus,.btn.btn-purple:focus{background:#8f00e1}button.btn-wire-purple,.btn.btn-wire-purple{background:transparent;box-shadow:none;border:2px solid rgb(188,72,255);color:#fffc}button.btn-wire-purple i.btn-match,.btn.btn-wire-purple i.btn-match{color:#bc48ff}button.btn-wire-purple:hover,.btn.btn-wire-purple:hover{background:#bc48ff0d;border-color:#a915ff;color:#a915ff;box-shadow:none}button.btn-wire-purple:focus,.btn.btn-wire-purple:focus{border-color:#8f00e1;background:transparent}button.btn-purple-alt,.btn.btn-purple-alt{background:#692fbd;box-shadow:0 2px #3c1b6b}button.btn-purple-alt:hover,.btn.btn-purple-alt:hover{box-shadow:0 2px 0 1px #3c1b6b inset}button.btn-purple-alt:focus,.btn.btn-purple-alt:focus{background:#3c1b6b}button.btn-wire-purple-alt,.btn.btn-wire-purple-alt{background:transparent;box-shadow:none;border:2px solid rgb(105,47,189);color:#fffc}button.btn-wire-purple-alt i.btn-match,.btn.btn-wire-purple-alt i.btn-match{color:#692fbd}button.btn-wire-purple-alt:hover,.btn.btn-wire-purple-alt:hover{background:#692fbd0d;border-color:#522594;color:#522594;box-shadow:none}button.btn-wire-purple-alt:focus,.btn.btn-wire-purple-alt:focus{border-color:#3c1b6b;background:transparent}button.btn-neutral,.btn.btn-neutral{background:#464646;box-shadow:0 2px #131313}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #131313 inset}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#131313}button.btn-wire-neutral,.btn.btn-wire-neutral{background:transparent;box-shadow:none;border:2px solid rgb(70,70,70);color:#fffc}button.btn-wire-neutral i.btn-match,.btn.btn-wire-neutral i.btn-match{color:#464646}button.btn-wire-neutral:hover,.btn.btn-wire-neutral:hover{background:#4646460d;border-color:#2d2d2d;color:#2d2d2d;box-shadow:none}button.btn-wire-neutral:focus,.btn.btn-wire-neutral:focus{border-color:#131313;background:transparent}button.btn-yellow,.btn.btn-yellow{background:#e5a123;box-shadow:0 2px #916411}button.btn-yellow:hover,.btn.btn-yellow:hover{box-shadow:0 2px 0 1px #916411 inset}button.btn-yellow:focus,.btn.btn-yellow:focus{background:#916411}button.btn-wire-yellow,.btn.btn-wire-yellow{background:transparent;box-shadow:none;border:2px solid rgb(229,161,35);color:#fffc}button.btn-wire-yellow i.btn-match,.btn.btn-wire-yellow i.btn-match{color:#e5a123}button.btn-wire-yellow:hover,.btn.btn-wire-yellow:hover{background:#e5a1230d;border-color:#be8417;color:#be8417;box-shadow:none}button.btn-wire-yellow:focus,.btn.btn-wire-yellow:focus{border-color:#916411;background:transparent}button.btn-white,.btn.btn-white{background:#fff;box-shadow:0 2px #ccc}button.btn-white:hover,.btn.btn-white:hover{box-shadow:0 2px 0 1px #ccc inset}button.btn-white:focus,.btn.btn-white:focus{background:#ccc}button.btn-wire-white,.btn.btn-wire-white{background:transparent;box-shadow:none;border:2px solid rgb(255,255,255);color:#fffc}button.btn-wire-white i.btn-match,.btn.btn-wire-white i.btn-match{color:#fff}button.btn-wire-white:hover,.btn.btn-wire-white:hover{background:#ffffff0d;border-color:#e6e6e6;color:#e6e6e6;box-shadow:none}button.btn-wire-white:focus,.btn.btn-wire-white:focus{border-color:#ccc;background:transparent}button.btn-black,.btn.btn-black{background:#000;box-shadow:0 2px #000}button.btn-black:hover,.btn.btn-black:hover{box-shadow:0 2px 0 1px #000 inset}button.btn-black:focus,.btn.btn-black:focus{background:#000}button.btn-wire-black,.btn.btn-wire-black{background:transparent;box-shadow:none;border:2px solid rgb(0,0,0);color:#fffc}button.btn-wire-black i.btn-match,.btn.btn-wire-black i.btn-match{color:#000}button.btn-wire-black:hover,.btn.btn-wire-black:hover{background:#0000000d;border-color:#000;color:#000;box-shadow:none}button.btn-wire-black:focus,.btn.btn-wire-black:focus{border-color:#000;background:transparent}button.btn-youtube,.btn.btn-youtube{background:#fd3c01;box-shadow:0 2px #972401}button.btn-youtube:hover,.btn.btn-youtube:hover{box-shadow:0 2px 0 1px #972401 inset}button.btn-youtube:focus,.btn.btn-youtube:focus{background:#972401}button.btn-wire-youtube,.btn.btn-wire-youtube{background:transparent;box-shadow:none;border:2px solid rgb(253,60,1);color:#fffc}button.btn-wire-youtube i.btn-match,.btn.btn-wire-youtube i.btn-match{color:#fd3c01}button.btn-wire-youtube:hover,.btn.btn-wire-youtube:hover{background:#fd3c010d;border-color:#ca3001;color:#ca3001;box-shadow:none}button.btn-wire-youtube:focus,.btn.btn-wire-youtube:focus{border-color:#972401;background:transparent}button.btn-raspbian,.btn.btn-raspbian{background:#b61240;box-shadow:0 2px #59091f}button.btn-raspbian:hover,.btn.btn-raspbian:hover{box-shadow:0 2px 0 1px #59091f inset}button.btn-raspbian:focus,.btn.btn-raspbian:focus{background:#59091f}button.btn-wire-raspbian,.btn.btn-wire-raspbian{background:transparent;box-shadow:none;border:2px solid rgb(182,18,64);color:#fffc}button.btn-wire-raspbian i.btn-match,.btn.btn-wire-raspbian i.btn-match{color:#b61240}button.btn-wire-raspbian:hover,.btn.btn-wire-raspbian:hover{background:#b612400d;border-color:#880d30;color:#880d30;box-shadow:none}button.btn-wire-raspbian:focus,.btn.btn-wire-raspbian:focus{border-color:#59091f;background:transparent}button.btn-windows,.btn.btn-windows{background:#12a7e3;box-shadow:0 2px #0b6184}button.btn-windows:hover,.btn.btn-windows:hover{box-shadow:0 2px 0 1px #0b6184 inset}button.btn-windows:focus,.btn.btn-windows:focus{background:#0b6184}button.btn-wire-windows,.btn.btn-wire-windows{background:transparent;box-shadow:none;border:2px solid rgb(18,167,227);color:#fffc}button.btn-wire-windows i.btn-match,.btn.btn-wire-windows i.btn-match{color:#12a7e3}button.btn-wire-windows:hover,.btn.btn-wire-windows:hover{background:#12a7e30d;border-color:#0e84b4;color:#0e84b4;box-shadow:none}button.btn-wire-windows:focus,.btn.btn-wire-windows:focus{border-color:#0b6184;background:transparent}button.btn-macos,.btn.btn-macos{background:#b3bcc2;box-shadow:0 2px #7a8a95}button.btn-macos:hover,.btn.btn-macos:hover{box-shadow:0 2px 0 1px #7a8a95 inset}button.btn-macos:focus,.btn.btn-macos:focus{background:#7a8a95}button.btn-wire-macos,.btn.btn-wire-macos{background:transparent;box-shadow:none;border:2px solid rgb(179,188,194);color:#fffc}button.btn-wire-macos i.btn-match,.btn.btn-wire-macos i.btn-match{color:#b3bcc2}button.btn-wire-macos:hover,.btn.btn-wire-macos:hover{background:#b3bcc20d;border-color:#97a3ab;color:#97a3ab;box-shadow:none}button.btn-wire-macos:focus,.btn.btn-wire-macos:focus{border-color:#7a8a95;background:transparent}button.btn-debian,.btn.btn-debian{background:#cf084e;box-shadow:0 2px #6d0429}button.btn-debian:hover,.btn.btn-debian:hover{box-shadow:0 2px 0 1px #6d0429 inset}button.btn-debian:focus,.btn.btn-debian:focus{background:#6d0429}button.btn-wire-debian,.btn.btn-wire-debian{background:transparent;box-shadow:none;border:2px solid rgb(207,8,78);color:#fffc}button.btn-wire-debian i.btn-match,.btn.btn-wire-debian i.btn-match{color:#cf084e}button.btn-wire-debian:hover,.btn.btn-wire-debian:hover{background:#cf084e0d;border-color:#9e063b;color:#9e063b;box-shadow:none}button.btn-wire-debian:focus,.btn.btn-wire-debian:focus{border-color:#6d0429;background:transparent}button.btn-fedora,.btn.btn-fedora{background:#52a2da;box-shadow:0 2px #236ea3}button.btn-fedora:hover,.btn.btn-fedora:hover{box-shadow:0 2px 0 1px #236ea3 inset}button.btn-fedora:focus,.btn.btn-fedora:focus{background:#236ea3}button.btn-wire-fedora,.btn.btn-wire-fedora{background:transparent;box-shadow:none;border:2px solid rgb(82,162,218);color:#fffc}button.btn-wire-fedora i.btn-match,.btn.btn-wire-fedora i.btn-match{color:#52a2da}button.btn-wire-fedora:hover,.btn.btn-wire-fedora:hover{background:#52a2da0d;border-color:#2c8bcd;color:#2c8bcd;box-shadow:none}button.btn-wire-fedora:focus,.btn.btn-wire-fedora:focus{border-color:#236ea3;background:transparent}button.btn-ubuntu,.btn.btn-ubuntu{background:#d64514;box-shadow:0 2px #79270b}button.btn-ubuntu:hover,.btn.btn-ubuntu:hover{box-shadow:0 2px 0 1px #79270b inset}button.btn-ubuntu:focus,.btn.btn-ubuntu:focus{background:#79270b}button.btn-wire-ubuntu,.btn.btn-wire-ubuntu{background:transparent;box-shadow:none;border:2px solid rgb(214,69,20);color:#fffc}button.btn-wire-ubuntu i.btn-match,.btn.btn-wire-ubuntu i.btn-match{color:#d64514}button.btn-wire-ubuntu:hover,.btn.btn-wire-ubuntu:hover{background:#d645140d;border-color:#a73610;color:#a73610;box-shadow:none}button.btn-wire-ubuntu:focus,.btn.btn-wire-ubuntu:focus{border-color:#79270b;background:transparent}button.btn-suse,.btn.btn-suse{background:#6fb425;box-shadow:0 2px #3b5f14}button.btn-suse:hover,.btn.btn-suse:hover{box-shadow:0 2px 0 1px #3b5f14 inset}button.btn-suse:focus,.btn.btn-suse:focus{background:#3b5f14}button.btn-wire-suse,.btn.btn-wire-suse{background:transparent;box-shadow:none;border:2px solid rgb(111,180,37);color:#fffc}button.btn-wire-suse i.btn-match,.btn.btn-wire-suse i.btn-match{color:#6fb425}button.btn-wire-suse:hover,.btn.btn-wire-suse:hover{background:#6fb4250d;border-color:#558a1c;color:#558a1c;box-shadow:none}button.btn-wire-suse:focus,.btn.btn-wire-suse:focus{border-color:#3b5f14;background:transparent}button.btn-redhat,.btn.btn-redhat{background:#c60200;box-shadow:0 2px #600100}button.btn-redhat:hover,.btn.btn-redhat:hover{box-shadow:0 2px 0 1px #600100 inset}button.btn-redhat:focus,.btn.btn-redhat:focus{background:#600100}button.btn-wire-redhat,.btn.btn-wire-redhat{background:transparent;box-shadow:none;border:2px solid rgb(198,2,0);color:#fffc}button.btn-wire-redhat i.btn-match,.btn.btn-wire-redhat i.btn-match{color:#c60200}button.btn-wire-redhat:hover,.btn.btn-wire-redhat:hover{background:#c602000d;border-color:#930100;color:#930100;box-shadow:none}button.btn-wire-redhat:focus,.btn.btn-wire-redhat:focus{border-color:#600100;background:transparent}button.btn-centos,.btn.btn-centos{background:#9b4c88;box-shadow:0 2px #572a4c}button.btn-centos:hover,.btn.btn-centos:hover{box-shadow:0 2px 0 1px #572a4c inset}button.btn-centos:focus,.btn.btn-centos:focus{background:#572a4c}button.btn-wire-centos,.btn.btn-wire-centos{background:transparent;box-shadow:none;border:2px solid rgb(155,76,136);color:#fffc}button.btn-wire-centos i.btn-match,.btn.btn-wire-centos i.btn-match{color:#9b4c88}button.btn-wire-centos:hover,.btn.btn-wire-centos:hover{background:#9b4c880d;border-color:#793b6a;color:#793b6a;box-shadow:none}button.btn-wire-centos:focus,.btn.btn-wire-centos:focus{border-color:#572a4c;background:transparent}button.btn-other,.btn.btn-other{background:#e5a123;box-shadow:0 2px #916411}button.btn-other:hover,.btn.btn-other:hover{box-shadow:0 2px 0 1px #916411 inset}button.btn-other:focus,.btn.btn-other:focus{background:#916411}button.btn-wire-other,.btn.btn-wire-other{background:transparent;box-shadow:none;border:2px solid rgb(229,161,35);color:#fffc}button.btn-wire-other i.btn-match,.btn.btn-wire-other i.btn-match{color:#e5a123}button.btn-wire-other:hover,.btn.btn-wire-other:hover{background:#e5a1230d;border-color:#be8417;color:#be8417;box-shadow:none}button.btn-wire-other:focus,.btn.btn-wire-other:focus{border-color:#916411;background:transparent}.alert.alert-warning{color:#e56723;background:#e5672333}.alert.alert-info{color:#027bff;background:#027bff33}.alert.alert-info-alt{color:#075cb7;background:#075cb733}.alert.alert-success{color:#0eef5f;background:#0eef5f33}.alert.alert-success-alt{color:#11a948;background:#11a94833}.alert.alert-error{color:#ef0e5d;background:#ef0e5d33}.alert.alert-error-alt{color:#c20941;background:#c2094133}.alert.alert-danger{color:#ef0e5d;background:#ef0e5d33}.alert.alert-danger-alt{color:#c20941;background:#c2094133}.alert.alert-purple{color:#bc48ff;background:#bc48ff33}.alert.alert-purple-alt{color:#692fbd;background:#692fbd33}.alert.alert-neutral{color:#464646;background:#46464633}.alert.alert-yellow{color:#e5a123;background:#e5a12333}.alert.alert-white{color:#fff;background:#fff3}.alert.alert-black{color:#000;background:#0003}.alert.alert-youtube{color:#fd3c01;background:#fd3c0133}.alert.alert-raspbian{color:#b61240;background:#b6124033}.alert.alert-windows{color:#12a7e3;background:#12a7e333}.alert.alert-macos{color:#b3bcc2;background:#b3bcc233}.alert.alert-debian{color:#cf084e;background:#cf084e33}.alert.alert-fedora{color:#52a2da;background:#52a2da33}.alert.alert-ubuntu{color:#d64514;background:#d6451433}.alert.alert-suse{color:#6fb425;background:#6fb42533}.alert.alert-redhat{color:#c60200;background:#c6020033}.alert.alert-centos{color:#9b4c88;background:#9b4c8833}.alert.alert-other{color:#e5a123;background:#e5a12333}@font-face{font-family:Sixtyfour;src:url(../../webfonts/Sixtyfour-Regular.ttf) format("truetype")}*{font-family:Roboto,Arial,"sans-serif";margin:0;padding:0;box-sizing:border-box}html{background-color:#111}body,html{height:100%;font-family:Arial,sans-serif}.container{display:flex;height:100vh}.horizontal{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}main{flex:1;display:flex;flex-direction:column}main .main-container{display:flex;flex-direction:column;flex:1;overflow:hidden;align-self:stretch}main .main-container .top-content{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:10px 10px 10px 15px;background:transparent;border-bottom:1px solid #222}main .main-container .top-content h1{color:#fff;font-weight:600;font-size:24px}main .main-container .top-content .top-actions{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}main .main-container .top-content .top-actions.align-right{justify-content:flex-end;margin-right:10px}main .main-container .top-content .top-actions .btn:first-child{margin-left:0!important}main .main-container .top-content .top-actions .btn,main .main-container .top-content .top-actions button{margin-left:10px}main .main-container .bottom-content{display:flex;flex-direction:row;align-self:stretch;justify-content:flex-start;align-items:flex-start;flex:1;overflow-y:auto;background:radial-gradient(circle at 0% 53%,rgba(239,14,93,.8) 10%,transparent 45%),radial-gradient(circle at 135% 53%,rgba(2,123,255,.8) 10%,transparent 95%),radial-gradient(circle at 50% 80%,rgba(14,239,95,.8) 40%,transparent 95%)}main .main-container .bottom-content .page-content{flex:2;overflow-y:auto;align-self:stretch;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;background:#000c;padding:5px}main .main-container .bottom-content .page-content .inner{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;padding:10px 10px 40px;background:#111;align-self:stretch}main .main-container .bottom-content .page-content .inner h1,main .main-container .bottom-content .page-content .inner h2,main .main-container .bottom-content .page-content .inner h3,main .main-container .bottom-content .page-content .inner h4,main .main-container .bottom-content .page-content .inner h5,main .main-container .bottom-content .page-content .inner h6{color:#ddd}main .main-container .bottom-content .page-content .inner p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;color:#666}main .main-container .bottom-content .page-panel{flex:1;overflow-y:auto;align-self:stretch;background:#111;border-top:none}main .main-container .bottom-content .page-panel.left-panel{border-right:1px solid #222;border-left:none}main .main-container .bottom-content .page-panel.left-panel.explr-explorer{flex:.5;overflow-y:auto;padding:0;background:#111;box-shadow:1px 1px .5px .5px inset #0003;max-width:250px}main .main-container .bottom-content .page-panel.right-panel{border-left:1px solid #222;border-right:none}.invisible{visibility:hidden!important}.hidden{display:none!important}.tac{text-align:center!important}.tar{text-align:right!important}a{text-decoration:none}.normal{font-weight:400!important}.bold{font-weight:700!important}.col{display:flex;flex:1;flex-direction:column;align-self:stretch}main .context-bar{padding:10px;position:sticky;top:0;z-index:1000;max-height:80px;border-bottom:1px solid #222;display:flex;flex-direction:row;align-items:center}main .context-bar .context-menu{flex:1}main .context-bar .context-menu .inner{display:flex}main .context-bar .context-menu .inner ul.pills{margin:0}main .context-bar .context-divider{width:1px;height:100%;background:#222;margin-left:20px;margin-right:20px}main .context-bar .contex-tail{margin-right:20px}main .context-bar .contex-tail .btn{margin-right:0}main .context-bar .context-user{display:flex;margin-right:20px}main .context-bar .context-user .trigger{color:#fff}main .context-bar .context-user .trigger .avatar{width:32px;height:32px;border-radius:4px;background:#aaa;margin-right:10px;display:flex;flex-direction:row;justify-content:center;align-items:center;text-align:center;font-weight:700;font-size:14px;border:1px solid #444;color:#000}main .context-bar .context-user .trigger i{margin-top:-5px;margin-left:10px}menu{width:300px;background:#111;overflow-y:auto;overflow-x:visible;padding:20px;z-index:2000;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;border-right:1px solid #222;min-width:64px}menu h1.logo{margin:40px 0 0 10px;align-self:stretch;display:flex}menu h1.logo a{text-align:center;text-shadow:0px 0 0 rgb(255,255,255),0px 2px 0 #444,0 0px 0 rgb(14,239,95),-0px 0 0 rgb(2,123,255),0 -0px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent;flex:1;font-family:Sixtyfour,Work Sans,Arial,"sans-serif";align-self:stretch;padding-right:3px;font-size:20px;text-transform:uppercase;transition:all .55s cubic-bezier(.19,1,.22,1);display:flex;flex-direction:row;justify-content:center;align-items:center;position:relative;color:#fff}menu h1.logo a img{flex-shrink:0;width:30px;margin-right:10px;position:absolute;left:5px;transition:all .55s cubic-bezier(.19,1,.22,1)}menu h1.logo a img.after{opacity:0}menu:hover h1.logo a{text-align:center;text-shadow:3px 0 0 rgb(255,255,255),3px 2px 0 #444,0 3px 0 rgb(14,239,95),-3px 0 0 rgb(2,123,255),0 -3px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}menu:hover h1.logo a img.before{opacity:0}menu:hover h1.logo a img.after{animation-duration:.2s;animation-name:logotouch}menu nav{display:flex;align-self:stretch;flex:1}menu nav ul{margin:60px 0 20px;flex:1;align-self:flex-start;display:flex;flex-direction:column;list-style:none}menu nav ul li{align-self:stretch;overflow:hidden;position:relative;transition:all .55s cubic-bezier(.19,1,.22,1);margin:10px 0;border-radius:4px}menu nav ul li a{color:#ffffffe6;font-size:16px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1;padding-top:5px;padding-bottom:5px;padding-left:10px}menu nav ul li a i{color:#fff;opacity:.2;background:transparent;display:flex;justify-content:center;align-items:center;align-self:stretch;padding:10px;width:40px;border-radius:4px;text-align:center;margin-right:20px}menu nav ul li:after{background:#fff;content:"";height:195px;left:-200px;opacity:.2;position:absolute;top:-50px;transform:rotate(35deg);transition:all .55s cubic-bezier(.19,1,.22,1);width:50px;z-index:-2;cursor:pointer}menu nav ul li.active a{color:#027bff;font-weight:700}menu nav ul li.active a i{opacity:1;color:#fff;background:#ffffffe6;background:#027bff}menu nav ul li:hover{background:#027bff}menu nav ul li:hover:after{z-index:2;left:120%;transition:all .55s cubic-bezier(.19,1,.22,1)}menu nav ul li:hover a{color:#fff;font-weight:700}menu nav ul li:hover a i{color:#fff;opacity:1}menu footer{background:#ffffff03;padding:20px 0;display:flex;flex-direction:row;align-self:stretch;text-align:center;justify-content:center}menu footer p{color:#444}menu footer p.version a{color:#777;font-weight:700}.dropdown{position:relative;display:flex;align-self:stretch}.dropdown.dropdown-show ul.dropdown-menu{display:flex;flex-direction:column}.dropdown .trigger{cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1}.dropdown ul.dropdown-menu{position:absolute;top:100%;left:0;display:none;background-color:#222;box-shadow:0 8px 16px #0003;z-index:1000;list-style-type:none;margin:0;overflow:hidden;border-radius:4px}.dropdown ul.dropdown-menu li{padding:8px 16px;cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;transition:all .55s cubic-bezier(.19,1,.22,1)}.dropdown ul.dropdown-menu li.danger:hover{background-color:#ef0e5d}.dropdown ul.dropdown-menu li:hover{background-color:#027bff}.dropdown ul.dropdown-menu li a{padding:8px 16px 8px 8px;color:#fff;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch}.dropdown ul.dropdown-menu li a i{margin-right:15px}@keyframes logotouch{0%{opacity:0}50%{opacity:1}to{opacity:0;left:27px}}@keyframes shakednd{0%{transform:rotate(-2deg)}to{transform:rotate(2deg)}}button,.btn{position:relative;padding:10px 10px 8px;font-size:14px;color:#fff;cursor:pointer;border:none;border-radius:4px;background:#027bff;box-shadow:0 2px #004a9b;font-weight:700;letter-spacing:-.5px;margin-top:-2px;min-width:38px;min-height:34px;text-align:center;justify-content:center}button i.icon-left,.btn i.icon-left{margin-right:5px}button:hover,.btn:hover{box-shadow:0 2px 0 1px #004a9b inset;color:#fffc}button:focus,.btn:focus{background:#004a9b;color:#ffffff80;box-shadow:none}button.btn-pixel,.btn.btn-pixel{background:#ccc;border:1px solid transparent;transition:all .55s cubic-bezier(.19,1,.22,1);text-transform:uppercase;font-size:12px;box-shadow:4px 0 #fff,0 4px #0eef5f,-4px 0 #027bff,0 -4px #ef0e5d;color:#222;overflow:hidden}button.btn-pixel:hover,.btn.btn-pixel:hover{box-shadow:6px 0 #fff,0 6px #0eef5f,-6px 0 #027bff,0 -6px #ef0e5d;text-align:center;text-shadow:4px 0 0 rgb(255,255,255),4px 2px 0 #444,0 4px 0 rgb(14,239,95),-4px 0 0 rgb(2,123,255),0 -4px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}button.btn-neutral,.btn.btn-neutral{color:#fff;background:#555;box-shadow:0 2px #3c3c3c;border:1px solid transparent}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #222 inset;background:#3c3c3c}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#222;border:1px solid #AAA}button .btn-wire-neutral,.btn .btn-wire-neutral{background:transparent;border:2px solid #555;color:#fffc;box-shadow:none}button .btn-wire-neutral:hover,.btn .btn-wire-neutral:hover{background:#5555550d;border-color:#3c3c3c;color:#3c3c3c;box-shadow:none}button .btn-wire-neutral:focus,.btn .btn-wire-neutral:focus{border-color:#222;background:transparent}button.btn-naked,.btn.btn-naked{background:transparent;box-shadow:none;border:1px solid transparent;color:#fff}button.btn-naked:hover,.btn.btn-naked:hover{box-shadow:0 2px 0 1px #222 inset;background:#3c3c3c;color:#fff}button.btn-naked:focus,.btn.btn-naked:focus{background:#222;border:1px solid #AAA}button i.main,.btn i.main{font-size:18px}button sup,button sub,.btn sup,.btn sub{position:absolute;top:-4px;right:-4px;background:#777;border-bottom:2px solid #555;color:red;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}button sup i,button sub i,.btn sup i,.btn sub i{color:#fff;font-size:10px}button.btn-double-icon,.btn.btn-double-icon{margin-right:5px}button.disabled,.btn.disabled{cursor:default}.alert{padding:20px;align-self:stretch;display:flex;flex-direction:row;justify-content:center;align-items:center;border-radius:4px}.alert i{margin-right:13px}.alert a{color:inherit;margin-left:4px;margin-right:4px;text-decoration:underline}ul.explr-tree{height:100%!important}ul.explr-tree ul{padding-top:0!important}ul.explr-tree li{position:relative}ul.explr-tree li span{color:#555;font-size:13px;padding-left:5px;cursor:pointer}ul.explr-tree li span.explr-plus,ul.explr-tree li span.explr-minus{z-index:1}ul.explr-tree li span.explr-plus:hover,ul.explr-tree li span.explr-minus:hover{color:#aaa}ul.explr-tree li i.main{font-size:14px}ul.explr-tree li sup,ul.explr-tree li sub{position:absolute;top:0;left:5px;background:#777;border-bottom:2px solid #555;border-radius:4px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-tree li sup i,ul.explr-tree li sub i{color:#fff;font-size:4px!important;margin-bottom:0}ul.explr-tree li a{color:#fff;padding-right:80px;margin-top:2px}ul.explr-tree li a:hover{color:#fff}ul.explr-tree li a.active{background:#ffffff1a;border-radius:4px;font-weight:700;text-decoration:underline;margin-left:35px;padding-left:5px;margin-right:10px}.explr-multiselection-actions,.explr-selection-actions{display:none;flex-direction:row;justify-content:flex-end;align-items:center;flex:1}.explr-multiselection-actions button,.explr-selection-actions button{display:none}body.explr-selection-actionable .explr-selection-actions,body.explr-selection-actionable.explr-selection-folder .explr-selection-actions button.explr-selection-folder,body.explr-selection-actionable.explr-selection-entity .explr-selection-actions button.explr-selection-entity,body.explr-multiselection-actionable .explr-multiselection-actions,body.explr-multiselection-actionable.explr-multiselection-folder .explr-multiselection-actions button.explr-multiselection-folder,body.explr-multiselection-actionable.explr-multiselection-entity .explr-multiselection-actions button.explr-multiselection-entity{display:flex}.selectable-zone{flex:1;align-self:stretch;border:1px solid transparent}ul.explr-dirview{display:flex;flex-direction:row;flex-wrap:wrap}ul.explr-dirview li{display:flex;flex-direction:column;justify-content:flex-start;align-items:center;flex-shrink:0;margin:10px;min-width:100px;min-height:130px;padding-top:5px;border:1px solid transparent;border-radius:4px}ul.explr-dirview li.renaming a span{display:none}ul.explr-dirview li.renaming a form{display:block}ul.explr-dirview li.highlight-drop{border:1px dotted rgba(2,123,255,.4);background:#027bff4d}ul.explr-dirview li.highlight-clicked{border:1px dotted rgba(255,255,255,.2);background:#ffffff1a}ul.explr-dirview li a{color:#bbb;text-decoration:none;flex:1;text-align:center;font-size:12px;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;max-width:84px;min-width:84px;position:relative;word-break:break-all}ul.explr-dirview li a.with-thumbnail .img-holder{width:64px;height:64px;background:#000;border-radius:8px;display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden;margin-bottom:12px}ul.explr-dirview li a.with-thumbnail .img-holder img{max-height:100%;max-width:100%}ul.explr-dirview li a.with-thumbnail i{font-size:24px;position:absolute;top:-4px;left:-4px;text-shadow:0 .5px .5px #777}ul.explr-dirview li a i{font-size:64px;margin-bottom:12px;border-radius:8px}ul.explr-dirview li a sup,ul.explr-dirview li a sub{position:absolute;top:-2px;right:0;background:#777;border-bottom:2px solid #555;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-dirview li a sup i,ul.explr-dirview li a sub i{color:#fff;font-size:10px;margin-bottom:0}ul.explr-dirview li a input{width:100%;padding:0 3px}ul.explr-dirview li a input:focus{outline:none}ul.explr-dirview li a:hover{opacity:.8}ul.explr-dirview li a form{display:none}ul.explr-dirview li.new-folder a{color:#027bff}ul.explr-dirview li.new-folder a form{display:block}ul.explr-dirview .ui-draggable-dragging{z-index:20}ul.explr-dirview .ui-draggable-dragging a{opacity:1!important}.modal-explr-picker h2{margin-top:0}.modal-explr-picker .explr-tree{width:400px;max-width:400px;max-height:300px;overflow:auto;background:#222;padding:10px 20px;border-radius:4px;margin-top:15px}.content-explr-picker{cursor:pointer}.selection-rectangle{position:absolute;border:1px solid rgb(0,153,255);background-color:#0099ff1a;pointer-events:none;z-index:1000}ul.pills{background:#222;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #0003;border:1px solid #222;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0}ul.pills li.divider{margin:0 20px;width:1px;height:100%;background:#333}ul.pills li:hover a{opacity:.9}ul.pills li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#fff;overflow:hidden;padding-right:30px;text-align:center;background:#0003;margin-right:5px;transition:all .25s cubic-bezier(.19,1,.22,1)}ul.pills li a span{display:flex;justify-content:center;align-items:center;margin-right:20px;height:42px;background:#0003;width:42px}ul.pills li.active a{color:#333;background:#ccc;font-weight:700}ul.pills li:hover a{color:#fff;background:#027bff}ul.pills li:last-child a{margin-right:0}.breadcrumb-container{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;border-bottom:1px solid #222;background:transparent;padding:10px}.breadcrumb-container ul.breadcrumb{background:#222;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #0003;border:1px solid #222;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0;overflow-x:auto;max-width:65vw;white-space:nowrap}.breadcrumb-container ul.breadcrumb li{display:inline-block}.breadcrumb-container ul.breadcrumb li.divider{margin:0 5px}.breadcrumb-container ul.breadcrumb li.divider i{color:#aaa}.breadcrumb-container ul.breadcrumb li span,.breadcrumb-container ul.breadcrumb li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#fff;text-align:center;padding:0 3px}.breadcrumb-container ul.breadcrumb li span i,.breadcrumb-container ul.breadcrumb li a i{margin-right:5px}.breadcrumb-container ul.breadcrumb li:hover a{color:#fff;background:#027bff}.breadcrumb-container ul.breadcrumb li:last-child a{margin-right:0}.pickers,.modals{position:fixed;background:#0006;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:10000}.pickers.pickers .modals-outer .modals-inner .modal h2,.modals.pickers .modals-outer .modals-inner .modal h2{font-size:14px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.pickers .modals-outer,.modals .modals-outer{min-width:464px;max-width:464px;display:flex;flex-direction:column;overflow:auto;padding-bottom:2px}.pickers .modals-outer .modals-inner,.modals .modals-outer .modals-inner{background:#111;border-radius:10px;color:#333;padding:40px;box-shadow:0 2px #222;border:1px solid #222}.pickers .modals-outer .modals-inner .modal h2,.modals .modals-outer .modals-inner .modal h2{padding:0;margin:0 0 30px;font-weight:400;color:#999}.pickers .modals-outer .modals-inner .modal h3,.modals .modals-outer .modals-inner .modal h3{align-self:stretch;margin:0 0 10px;font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#fff;padding-bottom:10px;text-decoration:none}.pickers .modals-outer .modals-inner .modal h3.divide,.modals .modals-outer .modals-inner .modal h3.divide{border-top:1px solid #222;margin-top:20px;padding-top:20px}.toast{visibility:hidden;min-width:250px;margin-left:-125px;background-color:#333;color:#fff;text-align:center;border-radius:4px;padding:16px;position:fixed;z-index:1;left:50%;bottom:30px;font-size:17px;border:2px dashed #222}.toast.show{visibility:visible;-webkit-animation:fadein .5s,fadeout .5s 2.5s;animation:fadein .5s,fadeout .5s 2.5s}@-webkit-keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@-webkit-keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}@keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}body.dragover .shakeondrag{animation:shakednd .1s linear alternate infinite}.btn-super-upload-busy,.btn-super-upload{display:flex;flex-direction:row;justify-content:center;align-items:center;margin-left:10px;position:relative}.btn-super-upload-busy.btn-super-upload-busy,.btn-super-upload.btn-super-upload-busy{border:none!important}.btn-super-upload-busy .unprogress,.btn-super-upload .unprogress{display:block}.btn-super-upload-busy .progress,.btn-super-upload .progress{display:none;width:200px;height:10px;background:#666;border-radius:4px;flex-direction:row;justify-content:flex-start;align-items:center}.btn-super-upload-busy .progress .progress-bar,.btn-super-upload .progress .progress-bar{border-radius:4px;background-color:#027bff;height:100%}.btn-super-upload-busy .progress .percent,.btn-super-upload .progress .percent{display:flex;justify-content:center;align-items:center;position:absolute;left:0;right:0;bottom:0;top:2px;font-size:15px;color:#fff;text-shadow:0 0 2px rgb(0,0,0)}.btn-super-upload-busy.uploading .progress,.btn-super-upload.uploading .progress{display:block}.btn-super-upload-busy.uploading .unprogress,.btn-super-upload.uploading .unprogress{display:none}.panes{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.panes .pane-section,.panes tbody{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:1px 1px 28px;background:#111;border-radius:4px;border:4px solid rgba(255,255,255,.05)}.panes .pane-section:hover,.panes tbody:hover,.panes .pane-section:hover tr.title-item,.panes tbody:hover tr.title-item{border-color:#027bff0d}.panes .pane-section .pane-item,.panes .pane-section tr,.panes tbody .pane-item,.panes tbody tr{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:8px 18px;background:#222}.panes .pane-section .pane-item:nth-child(odd),.panes .pane-section tr:nth-child(odd),.panes tbody .pane-item:nth-child(odd),.panes tbody tr:nth-child(odd){background-color:#111}.panes .pane-section .pane-item:nth-child(odd) td.description,.panes .pane-section tr:nth-child(odd) td.description,.panes tbody .pane-item:nth-child(odd) td.description,.panes tbody tr:nth-child(odd) td.description{color:#ffffffb3}.panes .pane-section .pane-item:nth-child(2n),.panes .pane-section tr:nth-child(2n),.panes tbody .pane-item:nth-child(2n),.panes tbody tr:nth-child(2n){background-color:#141414}.panes .pane-section .pane-item .pane-cell,.panes .pane-section .pane-item td,.panes .pane-section tr .pane-cell,.panes .pane-section tr td,.panes tbody .pane-item .pane-cell,.panes tbody .pane-item td,.panes tbody tr .pane-cell,.panes tbody tr td{color:#888;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;flex:1;font-size:14px}.panes .pane-section .pane-item .pane-cell.vertical,.panes .pane-section .pane-item td.vertical,.panes .pane-section tr .pane-cell.vertical,.panes .pane-section tr td.vertical,.panes tbody .pane-item .pane-cell.vertical,.panes tbody .pane-item td.vertical,.panes tbody tr .pane-cell.vertical,.panes tbody tr td.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}.panes .pane-section .pane-item .pane-cell.description,.panes .pane-section .pane-item td.description,.panes .pane-section tr .pane-cell.description,.panes .pane-section tr td.description,.panes tbody .pane-item .pane-cell.description,.panes tbody .pane-item td.description,.panes tbody tr .pane-cell.description,.panes tbody tr td.description{align-self:stretch}.panes .pane-section .pane-item .pane-cell.value,.panes .pane-section .pane-item td.value,.panes .pane-section tr .pane-cell.value,.panes .pane-section tr td.value,.panes tbody .pane-item .pane-cell.value,.panes tbody .pane-item td.value,.panes tbody tr .pane-cell.value,.panes tbody tr td.value{flex:0;margin-left:20px;word-break:break-all;flex-basis:auto}.panes .pane-section .pane-item .pane-cell.value i.icon-legend,.panes .pane-section .pane-item td.value i.icon-legend,.panes .pane-section tr .pane-cell.value i.icon-legend,.panes .pane-section tr td.value i.icon-legend,.panes tbody .pane-item .pane-cell.value i.icon-legend,.panes tbody .pane-item td.value i.icon-legend,.panes tbody tr .pane-cell.value i.icon-legend,.panes tbody tr td.value i.icon-legend{font-size:10px;margin-right:10px}.panes .pane-section .pane-item .pane-cell.value i.icon-value,.panes .pane-section .pane-item td.value i.icon-value,.panes .pane-section tr .pane-cell.value i.icon-value,.panes .pane-section tr td.value i.icon-value,.panes tbody .pane-item .pane-cell.value i.icon-value,.panes tbody .pane-item td.value i.icon-value,.panes tbody tr .pane-cell.value i.icon-value,.panes tbody tr td.value i.icon-value{padding:2px 2px 1px;border-radius:2px;width:16px;text-align:center}.panes .pane-section .pane-item.title-item,.panes .pane-section tr.title-item,.panes tbody .pane-item.title-item,.panes tbody tr.title-item{border-bottom:1px solid #222}.panes .pane-section .pane-item.title-item td,.panes .pane-section tr.title-item td,.panes tbody .pane-item.title-item td,.panes tbody tr.title-item td{color:#027bffe6;font-size:16px;font-weight:700}.panes .pane-section .pane-item.title-item td i,.panes .pane-section tr.title-item td i,.panes tbody .pane-item.title-item td i,.panes tbody tr.title-item td i{margin-right:10px}.panes .pane-section .pane-item.title-item td .more,.panes .pane-section tr.title-item td .more,.panes tbody .pane-item.title-item td .more,.panes tbody tr.title-item td .more{flex:1;text-align:right;font-size:12px;color:#888;font-weight:400;font-style:italic}.panes .pane-section .pane-item.variable-item,.panes .pane-section tr.variable-item,.panes tbody .pane-item.variable-item,.panes tbody tr.variable-item{cursor:pointer}.panes .pane-section .pane-item.variable-item:hover,.panes .pane-section tr.variable-item:hover,.panes tbody .pane-item.variable-item:hover,.panes tbody tr.variable-item:hover{background-color:#027bff0d}.panes .pane-section .pane-item.variable-item:hover td,.panes .pane-section tr.variable-item:hover td,.panes tbody .pane-item.variable-item:hover td,.panes tbody tr.variable-item:hover td{font-weight:700;color:#fff}.panes .pane-section .pane-item.variable-item:hover td i.icon-legend,.panes .pane-section tr.variable-item:hover td i.icon-legend,.panes tbody .pane-item.variable-item:hover td i.icon-legend,.panes tbody tr.variable-item:hover td i.icon-legend{color:#fff}.panes .pane-section .pane-item.variable-item:hover td span,.panes .pane-section .pane-item.variable-item:hover td i.icon-value,.panes .pane-section tr.variable-item:hover td span,.panes .pane-section tr.variable-item:hover td i.icon-value,.panes tbody .pane-item.variable-item:hover td span,.panes tbody .pane-item.variable-item:hover td i.icon-value,.panes tbody tr.variable-item:hover td span,.panes tbody tr.variable-item:hover td i.icon-value{background-color:#ffffff4d;color:#fff}.panes .pane-section .pane-item.variable-item:hover td.description,.panes .pane-section tr.variable-item:hover td.description,.panes tbody .pane-item.variable-item:hover td.description,.panes tbody tr.variable-item:hover td.description{color:#027bff}.tiles{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;align-self:stretch}.tiles .tiles-inner{display:flex;flex:1;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;align-self:stretch;padding:2px}.tiles .tiles-inner .tile-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#fff;margin:1px;padding:15px 10px 15px 15px;border-radius:4px;border-bottom:1px solid transparent}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-left:4px solid rgb(2,123,255);border-radius:4px;border-bottom:2px solid #171717;background:#222;color:#027bff}.tiles .tiles-inner .tile-item:hover:hover,.tiles .tiles-inner .tile-item.active:hover{opacity:1}.tiles .tiles-inner .tile-item:hover.disabled,.tiles .tiles-inner .tile-item.active.disabled{border-left-color:#444;color:#fff}.tiles .tiles-inner .tile-item:hover.starred,.tiles .tiles-inner .tile-item.active.starred{border-left-color:#e5a123;color:#e5a123}.tiles .tiles-inner .tile-item:hover.starred .tile-tail .head-icon i,.tiles .tiles-inner .tile-item.active.starred .tile-tail .head-icon i{opacity:1;color:#e5a123;font-size:8px}.tiles .tiles-inner .tile-item.starred .tile-tail .head-icon i{font-size:8px;color:#e5a123}.tiles .tiles-inner .tile-item.disabled .tile-body{opacity:.3}.tiles .tiles-inner .tile-item.disabled .tile-tail .head-icon i{color:#444;opacity:.4}.tiles .tiles-inner .tile-item .tile-tail{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin-left:10px;min-width:10px}.tiles .tiles-inner .tile-item .tile-tail .head-icon{flex:1;display:flex;text-align:right;flex-direction:row;justify-content:center;align-items:center;align-self:stretch}.tiles .tiles-inner .tile-item .tile-tail .head-icon i{font-size:6px;display:flex}.tiles .tiles-inner .tile-item .tile-tail .status-icons{display:flex;flex-direction:row;justify-content:flex-end;align-items:center}.tiles .tiles-inner .tile-item .tile-tail .status-icons i{font-size:16px;margin-left:10px}.tiles .tiles-inner .tile-item .tile-body{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;font-size:15px;font-weight:400;letter-spacing:.8px;line-height:22px;margin:0;flex-wrap:nowrap}.tiles .tiles-inner .tile-item .tile-body i{font-size:8px;margin-right:5px}.tiles .tiles-inner .tile-item .tile-metrics{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;margin:0}.tiles .tiles-inner .tile-item .tile-metrics .foot-span span,.tiles .tiles-inner .tile-item .tile-metrics .foot-span{opacity:.8;font-size:13px;font-family:Courier New}span.empty{background:#e5a1234d;color:#e5a123;text-transform:lowercase;border-radius:2px;padding:2px 4px;font-weight:700}.inner-empty{display:flex;flex:1;flex-direction:column;align-self:stretch;justify-content:center;align-items:center}.inner-empty i{font-size:90px;opacity:.3;text-shadow:0 -1px #333,0 0px .5px #444}.toggle{position:relative;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.toggle input{display:none}.toggle input:checked+label{background:#027bff;border:1px solid rgba(255,255,255,.1);box-shadow:0 2px 2px #222 inset}.toggle input:checked+label:after{content:"";display:block;border-radius:50%;margin-left:21px;width:18px;height:18px;transition:.2s;background:#9bcbff;box-shadow:0 2px #0063ce}.toggle label{width:44px;height:26px;border-radius:15px;background:#222;cursor:pointer;border:1px solid rgba(255,255,255,.1);box-shadow:0 2px 2px #111 inset}.toggle label:after{content:"";display:block;border-radius:50%;width:18px;height:18px;margin:3px;background:#777;box-shadow:0 2px #555555e6;transition:.2s}.form-holder{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.form-holder form{max-width:434px}form{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}form .alert{padding:8px 15px;font-size:14px;margin:0 0 25px}.from-group-condition{flex-direction:column;align-self:stretch;justify-content:flex-start;align-items:flex-start;display:flex}.form-group{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;width:100%;flex:1;margin-bottom:20px}.form-group label{flex:1;font-size:12px;line-height:18px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.form-group label.btn-upload{color:#fff;font-size:14px;flex:0;flex-basis:auto;margin-top:5px}.form-group label.btn-upload input[type=file]{display:none}.form-group label.btn-upload input[type=text]{margin-bottom:2px;margin-left:10px}.form-group label.btn-upload span.btn{padding-right:20px}.form-group label.btn-upload i{margin-left:3px;margin-right:10px}.form-group .widget{margin-top:10px;align-self:stretch;display:flex;flex-direction:row}.form-group .widget.vertical{flex-direction:column}.form-group .widget.vertical select,.form-group .widget.vertical input{align-self:stretch}.form-group .widget.vertical select:first-child,.form-group .widget.vertical input:first-child{margin-bottom:10px}.form-group .widget .btn{margin-left:10px}.form-group .widget.widget-unit select,.form-group .widget.widget-unit input{flex-grow:0;background:none;box-shadow:none;border:none;border-bottom:1px solid #333;border-radius:0;max-width:80px;padding-left:0;color:#fff;text-align:center}.form-group .widget.widget-unit span{font-size:12px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-left:5px;color:#555}.form-group .widget div{color:#ffffffb3;font-size:14px}.form-group .widget select,.form-group .widget input,.form-group .widget textarea{outline:none;padding:8px 0 5px 8px;border-radius:2px;border:1px solid rgba(255,255,255,.05);flex:1;background:#555;box-shadow:0 2px 1px #444,0 4px 2px #333 inset;color:#ddd;font-size:14px}.form-group .widget select.input-naked,.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.input-naked,.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.input-naked,.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{color:#555;background:none;box-shadow:none;border:none;border-bottom:1px solid #333;border-radius:0}.form-group .widget select.input-naked,.form-group .widget input.input-naked,.form-group .widget textarea.input-naked{padding-left:0;color:#bbb}.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{border:none;background:#000;border-radius:4px;padding-left:10px;padding-right:10px}.form-group.tab-select{border-bottom:1px solid #444;display:flex;flex-direction:row;position:relative;height:48px;padding:48px 0 0;flex:0;flex-basis:auto}.form-group.tab-select .widget{height:49px;margin-top:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;position:absolute;top:0;left:0;border-bottom:2px solid rgb(2,123,255);color:#027bff}.form-group.tab-select .widget select{border:none;background:none;box-shadow:none;padding:10px 35px 10px 10px;margin:0;color:inherit;appearance:none;-moz-appearance:none;-webkit-appearance:none;text-align:left;font-weight:700;cursor:pointer;border-radius:4px 4px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;z-index:2}.form-group.tab-select .widget i{margin-left:10px;margin-right:0}.form-group.tab-select .widget i.triangle{margin-top:-4px;margin-left:0;position:absolute;right:10px}.form-group.form-group-horizontal{margin:10px 0 20px;flex-direction:row;justify-content:flex-start;align-items:center}.form-group.form-group-horizontal .widget{margin:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.actions{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin:20px 0 0}.actions.actions-intermediate{margin:0}.actions button{margin-left:25px}.actions.actions-left{justify-content:flex-start}.actions.actions-left .btn{margin-left:0;margin-right:25px}.actions.actions-right{justify-content:flex-end}.actions.actions-right .btn{margin-left:10px;margin-right:0}.actions.actions-center{justify-content:center}.actions.actions-center .btn{margin-left:0;margin-right:0}.view-content-list main .main-container .page-content .inner{padding-bottom:10px}.view-content-list main .main-container .content-object-input{margin-bottom:6px}.view-content-list.dragover main .main-container .inner .dropzone{border-radius:4px;background:#ffffff1a;border:1px dashed rgba(255,255,255,.5)}.view-content-edit main .main-container .bottom-content .page-content{flex:1}.view-content-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px;flex:1}.view-content-edit main .main-container .bottom-content .page-panel.right-panel{flex:2;align-self:stretch;display:flex;flex-direction:column;overflow:hidden;justify-content:flex-start;align-items:center;padding:20px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3{color:#fff;padding:10px 10px 10px 0;margin-bottom:20px;font-size:16px;align-self:stretch;margin-left:-8px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 span{border-width:1px;border-style:solid;border-radius:4px;padding:4px 10px;margin-left:5px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 i{font-size:16px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper{display:flex;flex-direction:column;width:100%;position:relative;padding-top:56.25%;overflow:hidden;border-radius:4px;outline:4px solid rgba(255,255,255,.1)}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.view-logs-list main .main-container .bottom-content .page-content .inner{padding-top:8px;padding-bottom:8px}.view-logs-list main .main-container .bottom-content .page-content .logs{flex:1;display:flex;flex-direction:column;align-self:stretch}.view-logs-list main .main-container .bottom-content .page-content .logs pre{flex:1;background:#000000e6;border:1px solid rgba(85,85,85,.5);border-radius:4px;font-family:monospace;color:#f2f2f2;padding:20px;overflow:auto;align-self:stretch}.view-node-player-edit main .main-container .bottom-content .page-content{flex:1}.view-node-player-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px}.view-player-group-list main .main-container .players-holder ul.players{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #222;border-radius:4px;padding:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .head{display:flex;flex-direction:column;justify-content:center;align-items:center;color:#999;font-size:10px;padding:10px;cursor:default}.view-player-group-list main .main-container .players-holder ul.players li.player-item:hover .infos .title{color:#fff}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px;flex:1;max-width:180px;background:#000;border:1px solid #333;border-radius:4px;padding:3px 7px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .title{font-size:13px;color:#aaa;display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body{display:block;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#1b1b1b;padding:10px;align-self:stretch;flex:1;border-radius:4px;color:#ccc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px;font-size:12px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body span{opacity:.5;margin-right:7px;font-size:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a{color:#fff}.view-playlist-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.view-playlist-list main .main-container .modal-playlist-qrcode h2{text-align:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic{text-align:center;display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic img{border:4px solid #555;border-radius:4px}.view-playlist-list main .main-container .modal-slide h2{font-size:20px}.view-playlist-list main .main-container .modal-slide input[disabled]{color:#aaa}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select{margin-right:5px}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input{font-size:12px;max-width:50%}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input.datetimepicker{margin-left:5px;padding-left:0}.view-playlist-list main .main-container .bottom-content .page-content{flex:1}.view-playlist-list main .main-container .bottom-content .page-content.with-right-panel{flex:.5}.view-playlist-list main .main-container .bottom-content .page-content .inner{padding:0}.view-playlist-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#ddd;text-decoration:none;margin:0 0 20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder{margin:20px 20px 20px 10px;flex:1}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder{margin:20px 0 0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder form{max-width:initial}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder .form-group{flex-grow:0;margin-bottom:5px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder{position:relative}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .hover-only{display:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#fff;padding-bottom:10px;text-decoration:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4.divide{border-top:1px solid #222;margin-top:20px;padding-top:20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic{margin-top:10px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic img{border:1px dashed #555;padding:5px;border-radius:4px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview{background:#000;border:1px solid rgba(255,255,255,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview iframe{flex:1;align-self:stretch}.view-playlist-list main .main-container .bottom-content .page-content .inner .slides-holder{align-self:stretch;border-right:1px solid #222;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-playlist-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-player-group-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.view-player-group-list main .main-container .bottom-content .page-content{flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner{padding:0}.view-player-group-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#ddd;text-decoration:none;margin:0 0 20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder{margin:20px 20px 20px 10px;flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder{margin:20px 0 0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder form{max-width:initial}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder .form-group{flex-grow:0;margin-bottom:15px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder{position:relative}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .hover-only{display:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#fff;padding-bottom:10px;text-decoration:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4.divide{border-top:1px solid #222;margin-top:20px;padding-top:20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview{background:#000;border:1px solid rgba(255,255,255,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview iframe{flex:1;align-self:stretch}.view-player-group-list main .main-container .bottom-content .page-content .inner .players-holder{align-self:stretch;border-right:1px solid #222;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-player-group-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-playlist-list main .main-container .page-content .inner h3.divide{margin-top:50px}.view-playlist-list main .main-container .slides-holder ul.slides{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #222;border-radius:4px;padding:10px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .title{color:#333}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .type i{color:#333!important}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .body{opacity:0}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort{display:flex;flex-direction:column;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a{color:#999;font-size:10px;padding:10px;cursor:move}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a:hover{color:#027bff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px;flex:1;max-width:120px;background:#000;border:1px solid #333;border-radius:4px;padding:3px 7px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos:hover .title{color:#fff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .title{display:block;word-break:break-all;font-size:13px;color:#aaa;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#1b1b1b;padding:10px;align-self:stretch;flex:1;border-radius:4px;font-size:13px;color:#fff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;max-width:315px;overflow-x:auto;white-space:nowrap}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-bottom:8px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end{display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .prefix,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .prefix{margin-left:5px;margin-right:5px;font-size:12px;color:#ddd}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .cron-description,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .cron-description{display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;margin-left:5px;font-size:10px;opacity:.5}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a{color:#fff}.view-plugins-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-settings-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list .reboot{color:#ccc}.view-login main .main-container{position:relative}.view-login main .main-container .alert{position:absolute;top:0;left:0;right:0}.view-login main .main-container .login-content{display:flex;flex:1;flex-direction:column;justify-content:center;align-items:center}.view-login main .main-container .login-content .form-holder{width:400px;display:flex;justify-content:center;align-items:center;align-self:stretch;margin-left:auto;margin-right:auto}.view-login main .main-container .login-content .form-holder .card{display:flex;justify-content:center;align-items:center;align-self:stretch;border-radius:6px;padding:50px;color:#333}.view-login main .main-container .login-content .form-holder .card form{padding:0;flex:1;display:flex;justify-content:center;align-items:center;align-self:stretch}.view-login main .main-container .login-content .form-holder .card form .actions{margin-top:10px}.view-login main .main-container .login-content .form-holder .card form .actions .btn{padding-left:20px;padding-right:20px}.view-auth-user-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item.disabled .tile-body{opacity:.3;text-decoration-line:line-through}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-tail a:last-child{margin-left:10px} +.warning{color:#e56723!important}.bg-warning{background-color:#e56723!important}.border-warning{border-color:#e56723!important}.info{color:#027bff!important}.bg-info{background-color:#027bff!important}.border-info{border-color:#027bff!important}.info-alt{color:#075cb7!important}.bg-info-alt{background-color:#075cb7!important}.border-info-alt{border-color:#075cb7!important}.success{color:#0eef5f!important}.bg-success{background-color:#0eef5f!important}.border-success{border-color:#0eef5f!important}.success-alt{color:#11a948!important}.bg-success-alt{background-color:#11a948!important}.border-success-alt{border-color:#11a948!important}.error{color:#ef0e5d!important}.bg-error{background-color:#ef0e5d!important}.border-error{border-color:#ef0e5d!important}.error-alt{color:#c20941!important}.bg-error-alt{background-color:#c20941!important}.border-error-alt{border-color:#c20941!important}.danger{color:#ef0e5d!important}.bg-danger{background-color:#ef0e5d!important}.border-danger{border-color:#ef0e5d!important}.danger-alt{color:#c20941!important}.bg-danger-alt{background-color:#c20941!important}.border-danger-alt{border-color:#c20941!important}.purple{color:#bc48ff!important}.bg-purple{background-color:#bc48ff!important}.border-purple{border-color:#bc48ff!important}.purple-alt{color:#692fbd!important}.bg-purple-alt{background-color:#692fbd!important}.border-purple-alt{border-color:#692fbd!important}.neutral{color:#464646!important}.bg-neutral{background-color:#464646!important}.border-neutral{border-color:#464646!important}.yellow{color:#e5a123!important}.bg-yellow{background-color:#e5a123!important}.border-yellow{border-color:#e5a123!important}.white{color:#fff!important}.bg-white{background-color:#fff!important}.border-white{border-color:#fff!important}.black{color:#000!important}.bg-black{background-color:#000!important}.border-black{border-color:#000!important}.youtube{color:#fd3c01!important}.bg-youtube{background-color:#fd3c01!important}.border-youtube{border-color:#fd3c01!important}.raspbian{color:#b61240!important}.bg-raspbian{background-color:#b61240!important}.border-raspbian{border-color:#b61240!important}.windows{color:#12a7e3!important}.bg-windows{background-color:#12a7e3!important}.border-windows{border-color:#12a7e3!important}.macos{color:#b3bcc2!important}.bg-macos{background-color:#b3bcc2!important}.border-macos{border-color:#b3bcc2!important}.debian{color:#cf084e!important}.bg-debian{background-color:#cf084e!important}.border-debian{border-color:#cf084e!important}.fedora{color:#52a2da!important}.bg-fedora{background-color:#52a2da!important}.border-fedora{border-color:#52a2da!important}.ubuntu{color:#d64514!important}.bg-ubuntu{background-color:#d64514!important}.border-ubuntu{border-color:#d64514!important}.suse{color:#6fb425!important}.bg-suse{background-color:#6fb425!important}.border-suse{border-color:#6fb425!important}.redhat{color:#c60200!important}.bg-redhat{background-color:#c60200!important}.border-redhat{border-color:#c60200!important}.centos{color:#9b4c88!important}.bg-centos{background-color:#9b4c88!important}.border-centos{border-color:#9b4c88!important}.other{color:#e5a123!important}.bg-other{background-color:#e5a123!important}.border-other{border-color:#e5a123!important}button.btn-warning,.btn.btn-warning{background:#e56723;box-shadow:0 2px #913e11}button.btn-warning:hover,.btn.btn-warning:hover{box-shadow:0 2px 0 1px #913e11 inset}button.btn-warning:focus,.btn.btn-warning:focus{background:#913e11}button.btn-wire-warning,.btn.btn-wire-warning{background:transparent;box-shadow:none;border:2px solid rgb(229,103,35);color:#fffc}button.btn-wire-warning i.btn-match,.btn.btn-wire-warning i.btn-match{color:#e56723}button.btn-wire-warning:hover,.btn.btn-wire-warning:hover{background:#e567230d;border-color:#be5117;color:#be5117;box-shadow:none}button.btn-wire-warning:focus,.btn.btn-wire-warning:focus{border-color:#913e11;background:transparent}button.btn-info,.btn.btn-info{background:#027bff;box-shadow:0 2px #004a9b}button.btn-info:hover,.btn.btn-info:hover{box-shadow:0 2px 0 1px #004a9b inset}button.btn-info:focus,.btn.btn-info:focus{background:#004a9b}button.btn-wire-info,.btn.btn-wire-info{background:transparent;box-shadow:none;border:2px solid rgb(2,123,255);color:#fffc}button.btn-wire-info i.btn-match,.btn.btn-wire-info i.btn-match{color:#027bff}button.btn-wire-info:hover,.btn.btn-wire-info:hover{background:#027bff0d;border-color:#0063ce;color:#0063ce;box-shadow:none}button.btn-wire-info:focus,.btn.btn-wire-info:focus{border-color:#004a9b;background:transparent}button.btn-info-alt,.btn.btn-info-alt{background:#075cb7;box-shadow:0 2px #032b55}button.btn-info-alt:hover,.btn.btn-info-alt:hover{box-shadow:0 2px 0 1px #032b55 inset}button.btn-info-alt:focus,.btn.btn-info-alt:focus{background:#032b55}button.btn-wire-info-alt,.btn.btn-wire-info-alt{background:transparent;box-shadow:none;border:2px solid rgb(7,92,183);color:#fffc}button.btn-wire-info-alt i.btn-match,.btn.btn-wire-info-alt i.btn-match{color:#075cb7}button.btn-wire-info-alt:hover,.btn.btn-wire-info-alt:hover{background:#075cb70d;border-color:#054386;color:#054386;box-shadow:none}button.btn-wire-info-alt:focus,.btn.btn-wire-info-alt:focus{border-color:#032b55;background:transparent}button.btn-success,.btn.btn-success{background:#0eef5f;box-shadow:0 2px #088f39}button.btn-success:hover,.btn.btn-success:hover{box-shadow:0 2px 0 1px #088f39 inset}button.btn-success:focus,.btn.btn-success:focus{background:#088f39}button.btn-wire-success,.btn.btn-wire-success{background:transparent;box-shadow:none;border:2px solid rgb(14,239,95);color:#fffc}button.btn-wire-success i.btn-match,.btn.btn-wire-success i.btn-match{color:#0eef5f}button.btn-wire-success:hover,.btn.btn-wire-success:hover{background:#0eef5f0d;border-color:#0bbf4c;color:#0bbf4c;box-shadow:none}button.btn-wire-success:focus,.btn.btn-wire-success:focus{border-color:#088f39;background:transparent}button.btn-success-alt,.btn.btn-success-alt{background:#11a948;box-shadow:0 2px #084c21}button.btn-success-alt:hover,.btn.btn-success-alt:hover{box-shadow:0 2px 0 1px #084c21 inset}button.btn-success-alt:focus,.btn.btn-success-alt:focus{background:#084c21}button.btn-wire-success-alt,.btn.btn-wire-success-alt{background:transparent;box-shadow:none;border:2px solid rgb(17,169,72);color:#fffc}button.btn-wire-success-alt i.btn-match,.btn.btn-wire-success-alt i.btn-match{color:#11a948}button.btn-wire-success-alt:hover,.btn.btn-wire-success-alt:hover{background:#11a9480d;border-color:#0c7b34;color:#0c7b34;box-shadow:none}button.btn-wire-success-alt:focus,.btn.btn-wire-success-alt:focus{border-color:#084c21;background:transparent}button.btn-error,.btn.btn-error{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-error:hover,.btn.btn-error:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-error:focus,.btn.btn-error:focus{background:#8f0838}button.btn-wire-error,.btn.btn-wire-error{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#fffc}button.btn-wire-error i.btn-match,.btn.btn-wire-error i.btn-match{color:#ef0e5d}button.btn-wire-error:hover,.btn.btn-wire-error:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-error:focus,.btn.btn-wire-error:focus{border-color:#8f0838;background:transparent}button.btn-error-alt,.btn.btn-error-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-error-alt:hover,.btn.btn-error-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-error-alt:focus,.btn.btn-error-alt:focus{background:#610420}button.btn-wire-error-alt,.btn.btn-wire-error-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#fffc}button.btn-wire-error-alt i.btn-match,.btn.btn-wire-error-alt i.btn-match{color:#c20941}button.btn-wire-error-alt:hover,.btn.btn-wire-error-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-error-alt:focus,.btn.btn-wire-error-alt:focus{border-color:#610420;background:transparent}button.btn-danger,.btn.btn-danger{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-danger:hover,.btn.btn-danger:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-danger:focus,.btn.btn-danger:focus{background:#8f0838}button.btn-wire-danger,.btn.btn-wire-danger{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#fffc}button.btn-wire-danger i.btn-match,.btn.btn-wire-danger i.btn-match{color:#ef0e5d}button.btn-wire-danger:hover,.btn.btn-wire-danger:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-danger:focus,.btn.btn-wire-danger:focus{border-color:#8f0838;background:transparent}button.btn-danger-alt,.btn.btn-danger-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-danger-alt:hover,.btn.btn-danger-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-danger-alt:focus,.btn.btn-danger-alt:focus{background:#610420}button.btn-wire-danger-alt,.btn.btn-wire-danger-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#fffc}button.btn-wire-danger-alt i.btn-match,.btn.btn-wire-danger-alt i.btn-match{color:#c20941}button.btn-wire-danger-alt:hover,.btn.btn-wire-danger-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-danger-alt:focus,.btn.btn-wire-danger-alt:focus{border-color:#610420;background:transparent}button.btn-purple,.btn.btn-purple{background:#bc48ff;box-shadow:0 2px #8f00e1}button.btn-purple:hover,.btn.btn-purple:hover{box-shadow:0 2px 0 1px #8f00e1 inset}button.btn-purple:focus,.btn.btn-purple:focus{background:#8f00e1}button.btn-wire-purple,.btn.btn-wire-purple{background:transparent;box-shadow:none;border:2px solid rgb(188,72,255);color:#fffc}button.btn-wire-purple i.btn-match,.btn.btn-wire-purple i.btn-match{color:#bc48ff}button.btn-wire-purple:hover,.btn.btn-wire-purple:hover{background:#bc48ff0d;border-color:#a915ff;color:#a915ff;box-shadow:none}button.btn-wire-purple:focus,.btn.btn-wire-purple:focus{border-color:#8f00e1;background:transparent}button.btn-purple-alt,.btn.btn-purple-alt{background:#692fbd;box-shadow:0 2px #3c1b6b}button.btn-purple-alt:hover,.btn.btn-purple-alt:hover{box-shadow:0 2px 0 1px #3c1b6b inset}button.btn-purple-alt:focus,.btn.btn-purple-alt:focus{background:#3c1b6b}button.btn-wire-purple-alt,.btn.btn-wire-purple-alt{background:transparent;box-shadow:none;border:2px solid rgb(105,47,189);color:#fffc}button.btn-wire-purple-alt i.btn-match,.btn.btn-wire-purple-alt i.btn-match{color:#692fbd}button.btn-wire-purple-alt:hover,.btn.btn-wire-purple-alt:hover{background:#692fbd0d;border-color:#522594;color:#522594;box-shadow:none}button.btn-wire-purple-alt:focus,.btn.btn-wire-purple-alt:focus{border-color:#3c1b6b;background:transparent}button.btn-neutral,.btn.btn-neutral{background:#464646;box-shadow:0 2px #131313}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #131313 inset}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#131313}button.btn-wire-neutral,.btn.btn-wire-neutral{background:transparent;box-shadow:none;border:2px solid rgb(70,70,70);color:#fffc}button.btn-wire-neutral i.btn-match,.btn.btn-wire-neutral i.btn-match{color:#464646}button.btn-wire-neutral:hover,.btn.btn-wire-neutral:hover{background:#4646460d;border-color:#2d2d2d;color:#2d2d2d;box-shadow:none}button.btn-wire-neutral:focus,.btn.btn-wire-neutral:focus{border-color:#131313;background:transparent}button.btn-yellow,.btn.btn-yellow{background:#e5a123;box-shadow:0 2px #916411}button.btn-yellow:hover,.btn.btn-yellow:hover{box-shadow:0 2px 0 1px #916411 inset}button.btn-yellow:focus,.btn.btn-yellow:focus{background:#916411}button.btn-wire-yellow,.btn.btn-wire-yellow{background:transparent;box-shadow:none;border:2px solid rgb(229,161,35);color:#fffc}button.btn-wire-yellow i.btn-match,.btn.btn-wire-yellow i.btn-match{color:#e5a123}button.btn-wire-yellow:hover,.btn.btn-wire-yellow:hover{background:#e5a1230d;border-color:#be8417;color:#be8417;box-shadow:none}button.btn-wire-yellow:focus,.btn.btn-wire-yellow:focus{border-color:#916411;background:transparent}button.btn-white,.btn.btn-white{background:#fff;box-shadow:0 2px #ccc}button.btn-white:hover,.btn.btn-white:hover{box-shadow:0 2px 0 1px #ccc inset}button.btn-white:focus,.btn.btn-white:focus{background:#ccc}button.btn-wire-white,.btn.btn-wire-white{background:transparent;box-shadow:none;border:2px solid rgb(255,255,255);color:#fffc}button.btn-wire-white i.btn-match,.btn.btn-wire-white i.btn-match{color:#fff}button.btn-wire-white:hover,.btn.btn-wire-white:hover{background:#ffffff0d;border-color:#e6e6e6;color:#e6e6e6;box-shadow:none}button.btn-wire-white:focus,.btn.btn-wire-white:focus{border-color:#ccc;background:transparent}button.btn-black,.btn.btn-black{background:#000;box-shadow:0 2px #000}button.btn-black:hover,.btn.btn-black:hover{box-shadow:0 2px 0 1px #000 inset}button.btn-black:focus,.btn.btn-black:focus{background:#000}button.btn-wire-black,.btn.btn-wire-black{background:transparent;box-shadow:none;border:2px solid rgb(0,0,0);color:#fffc}button.btn-wire-black i.btn-match,.btn.btn-wire-black i.btn-match{color:#000}button.btn-wire-black:hover,.btn.btn-wire-black:hover{background:#0000000d;border-color:#000;color:#000;box-shadow:none}button.btn-wire-black:focus,.btn.btn-wire-black:focus{border-color:#000;background:transparent}button.btn-youtube,.btn.btn-youtube{background:#fd3c01;box-shadow:0 2px #972401}button.btn-youtube:hover,.btn.btn-youtube:hover{box-shadow:0 2px 0 1px #972401 inset}button.btn-youtube:focus,.btn.btn-youtube:focus{background:#972401}button.btn-wire-youtube,.btn.btn-wire-youtube{background:transparent;box-shadow:none;border:2px solid rgb(253,60,1);color:#fffc}button.btn-wire-youtube i.btn-match,.btn.btn-wire-youtube i.btn-match{color:#fd3c01}button.btn-wire-youtube:hover,.btn.btn-wire-youtube:hover{background:#fd3c010d;border-color:#ca3001;color:#ca3001;box-shadow:none}button.btn-wire-youtube:focus,.btn.btn-wire-youtube:focus{border-color:#972401;background:transparent}button.btn-raspbian,.btn.btn-raspbian{background:#b61240;box-shadow:0 2px #59091f}button.btn-raspbian:hover,.btn.btn-raspbian:hover{box-shadow:0 2px 0 1px #59091f inset}button.btn-raspbian:focus,.btn.btn-raspbian:focus{background:#59091f}button.btn-wire-raspbian,.btn.btn-wire-raspbian{background:transparent;box-shadow:none;border:2px solid rgb(182,18,64);color:#fffc}button.btn-wire-raspbian i.btn-match,.btn.btn-wire-raspbian i.btn-match{color:#b61240}button.btn-wire-raspbian:hover,.btn.btn-wire-raspbian:hover{background:#b612400d;border-color:#880d30;color:#880d30;box-shadow:none}button.btn-wire-raspbian:focus,.btn.btn-wire-raspbian:focus{border-color:#59091f;background:transparent}button.btn-windows,.btn.btn-windows{background:#12a7e3;box-shadow:0 2px #0b6184}button.btn-windows:hover,.btn.btn-windows:hover{box-shadow:0 2px 0 1px #0b6184 inset}button.btn-windows:focus,.btn.btn-windows:focus{background:#0b6184}button.btn-wire-windows,.btn.btn-wire-windows{background:transparent;box-shadow:none;border:2px solid rgb(18,167,227);color:#fffc}button.btn-wire-windows i.btn-match,.btn.btn-wire-windows i.btn-match{color:#12a7e3}button.btn-wire-windows:hover,.btn.btn-wire-windows:hover{background:#12a7e30d;border-color:#0e84b4;color:#0e84b4;box-shadow:none}button.btn-wire-windows:focus,.btn.btn-wire-windows:focus{border-color:#0b6184;background:transparent}button.btn-macos,.btn.btn-macos{background:#b3bcc2;box-shadow:0 2px #7a8a95}button.btn-macos:hover,.btn.btn-macos:hover{box-shadow:0 2px 0 1px #7a8a95 inset}button.btn-macos:focus,.btn.btn-macos:focus{background:#7a8a95}button.btn-wire-macos,.btn.btn-wire-macos{background:transparent;box-shadow:none;border:2px solid rgb(179,188,194);color:#fffc}button.btn-wire-macos i.btn-match,.btn.btn-wire-macos i.btn-match{color:#b3bcc2}button.btn-wire-macos:hover,.btn.btn-wire-macos:hover{background:#b3bcc20d;border-color:#97a3ab;color:#97a3ab;box-shadow:none}button.btn-wire-macos:focus,.btn.btn-wire-macos:focus{border-color:#7a8a95;background:transparent}button.btn-debian,.btn.btn-debian{background:#cf084e;box-shadow:0 2px #6d0429}button.btn-debian:hover,.btn.btn-debian:hover{box-shadow:0 2px 0 1px #6d0429 inset}button.btn-debian:focus,.btn.btn-debian:focus{background:#6d0429}button.btn-wire-debian,.btn.btn-wire-debian{background:transparent;box-shadow:none;border:2px solid rgb(207,8,78);color:#fffc}button.btn-wire-debian i.btn-match,.btn.btn-wire-debian i.btn-match{color:#cf084e}button.btn-wire-debian:hover,.btn.btn-wire-debian:hover{background:#cf084e0d;border-color:#9e063b;color:#9e063b;box-shadow:none}button.btn-wire-debian:focus,.btn.btn-wire-debian:focus{border-color:#6d0429;background:transparent}button.btn-fedora,.btn.btn-fedora{background:#52a2da;box-shadow:0 2px #236ea3}button.btn-fedora:hover,.btn.btn-fedora:hover{box-shadow:0 2px 0 1px #236ea3 inset}button.btn-fedora:focus,.btn.btn-fedora:focus{background:#236ea3}button.btn-wire-fedora,.btn.btn-wire-fedora{background:transparent;box-shadow:none;border:2px solid rgb(82,162,218);color:#fffc}button.btn-wire-fedora i.btn-match,.btn.btn-wire-fedora i.btn-match{color:#52a2da}button.btn-wire-fedora:hover,.btn.btn-wire-fedora:hover{background:#52a2da0d;border-color:#2c8bcd;color:#2c8bcd;box-shadow:none}button.btn-wire-fedora:focus,.btn.btn-wire-fedora:focus{border-color:#236ea3;background:transparent}button.btn-ubuntu,.btn.btn-ubuntu{background:#d64514;box-shadow:0 2px #79270b}button.btn-ubuntu:hover,.btn.btn-ubuntu:hover{box-shadow:0 2px 0 1px #79270b inset}button.btn-ubuntu:focus,.btn.btn-ubuntu:focus{background:#79270b}button.btn-wire-ubuntu,.btn.btn-wire-ubuntu{background:transparent;box-shadow:none;border:2px solid rgb(214,69,20);color:#fffc}button.btn-wire-ubuntu i.btn-match,.btn.btn-wire-ubuntu i.btn-match{color:#d64514}button.btn-wire-ubuntu:hover,.btn.btn-wire-ubuntu:hover{background:#d645140d;border-color:#a73610;color:#a73610;box-shadow:none}button.btn-wire-ubuntu:focus,.btn.btn-wire-ubuntu:focus{border-color:#79270b;background:transparent}button.btn-suse,.btn.btn-suse{background:#6fb425;box-shadow:0 2px #3b5f14}button.btn-suse:hover,.btn.btn-suse:hover{box-shadow:0 2px 0 1px #3b5f14 inset}button.btn-suse:focus,.btn.btn-suse:focus{background:#3b5f14}button.btn-wire-suse,.btn.btn-wire-suse{background:transparent;box-shadow:none;border:2px solid rgb(111,180,37);color:#fffc}button.btn-wire-suse i.btn-match,.btn.btn-wire-suse i.btn-match{color:#6fb425}button.btn-wire-suse:hover,.btn.btn-wire-suse:hover{background:#6fb4250d;border-color:#558a1c;color:#558a1c;box-shadow:none}button.btn-wire-suse:focus,.btn.btn-wire-suse:focus{border-color:#3b5f14;background:transparent}button.btn-redhat,.btn.btn-redhat{background:#c60200;box-shadow:0 2px #600100}button.btn-redhat:hover,.btn.btn-redhat:hover{box-shadow:0 2px 0 1px #600100 inset}button.btn-redhat:focus,.btn.btn-redhat:focus{background:#600100}button.btn-wire-redhat,.btn.btn-wire-redhat{background:transparent;box-shadow:none;border:2px solid rgb(198,2,0);color:#fffc}button.btn-wire-redhat i.btn-match,.btn.btn-wire-redhat i.btn-match{color:#c60200}button.btn-wire-redhat:hover,.btn.btn-wire-redhat:hover{background:#c602000d;border-color:#930100;color:#930100;box-shadow:none}button.btn-wire-redhat:focus,.btn.btn-wire-redhat:focus{border-color:#600100;background:transparent}button.btn-centos,.btn.btn-centos{background:#9b4c88;box-shadow:0 2px #572a4c}button.btn-centos:hover,.btn.btn-centos:hover{box-shadow:0 2px 0 1px #572a4c inset}button.btn-centos:focus,.btn.btn-centos:focus{background:#572a4c}button.btn-wire-centos,.btn.btn-wire-centos{background:transparent;box-shadow:none;border:2px solid rgb(155,76,136);color:#fffc}button.btn-wire-centos i.btn-match,.btn.btn-wire-centos i.btn-match{color:#9b4c88}button.btn-wire-centos:hover,.btn.btn-wire-centos:hover{background:#9b4c880d;border-color:#793b6a;color:#793b6a;box-shadow:none}button.btn-wire-centos:focus,.btn.btn-wire-centos:focus{border-color:#572a4c;background:transparent}button.btn-other,.btn.btn-other{background:#e5a123;box-shadow:0 2px #916411}button.btn-other:hover,.btn.btn-other:hover{box-shadow:0 2px 0 1px #916411 inset}button.btn-other:focus,.btn.btn-other:focus{background:#916411}button.btn-wire-other,.btn.btn-wire-other{background:transparent;box-shadow:none;border:2px solid rgb(229,161,35);color:#fffc}button.btn-wire-other i.btn-match,.btn.btn-wire-other i.btn-match{color:#e5a123}button.btn-wire-other:hover,.btn.btn-wire-other:hover{background:#e5a1230d;border-color:#be8417;color:#be8417;box-shadow:none}button.btn-wire-other:focus,.btn.btn-wire-other:focus{border-color:#916411;background:transparent}.alert.alert-warning{color:#e56723;background:#e5672333}.alert.alert-info{color:#027bff;background:#027bff33}.alert.alert-info-alt{color:#075cb7;background:#075cb733}.alert.alert-success{color:#0eef5f;background:#0eef5f33}.alert.alert-success-alt{color:#11a948;background:#11a94833}.alert.alert-error{color:#ef0e5d;background:#ef0e5d33}.alert.alert-error-alt{color:#c20941;background:#c2094133}.alert.alert-danger{color:#ef0e5d;background:#ef0e5d33}.alert.alert-danger-alt{color:#c20941;background:#c2094133}.alert.alert-purple{color:#bc48ff;background:#bc48ff33}.alert.alert-purple-alt{color:#692fbd;background:#692fbd33}.alert.alert-neutral{color:#464646;background:#46464633}.alert.alert-yellow{color:#e5a123;background:#e5a12333}.alert.alert-white{color:#fff;background:#fff3}.alert.alert-black{color:#000;background:#0003}.alert.alert-youtube{color:#fd3c01;background:#fd3c0133}.alert.alert-raspbian{color:#b61240;background:#b6124033}.alert.alert-windows{color:#12a7e3;background:#12a7e333}.alert.alert-macos{color:#b3bcc2;background:#b3bcc233}.alert.alert-debian{color:#cf084e;background:#cf084e33}.alert.alert-fedora{color:#52a2da;background:#52a2da33}.alert.alert-ubuntu{color:#d64514;background:#d6451433}.alert.alert-suse{color:#6fb425;background:#6fb42533}.alert.alert-redhat{color:#c60200;background:#c6020033}.alert.alert-centos{color:#9b4c88;background:#9b4c8833}.alert.alert-other{color:#e5a123;background:#e5a12333}@font-face{font-family:Sixtyfour;src:url(../../webfonts/Sixtyfour-Regular.ttf) format("truetype")}*{font-family:Roboto,Arial,"sans-serif";margin:0;padding:0;box-sizing:border-box}html{background-color:#111}body,html{height:100%;font-family:Arial,sans-serif}.container{display:flex;height:100vh}.horizontal{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}main{flex:1;display:flex;flex-direction:column}main .main-container{display:flex;flex-direction:column;flex:1;overflow:hidden;align-self:stretch}main .main-container .top-content{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:10px 10px 10px 15px;background:transparent;border-bottom:1px solid #222}main .main-container .top-content h1{color:#fff;font-weight:600;font-size:24px}main .main-container .top-content .top-actions{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}main .main-container .top-content .top-actions.align-right{justify-content:flex-end;margin-right:10px}main .main-container .top-content .top-actions .btn:first-child{margin-left:0!important}main .main-container .top-content .top-actions .btn,main .main-container .top-content .top-actions button{margin-left:10px}main .main-container .bottom-content{display:flex;flex-direction:row;align-self:stretch;justify-content:flex-start;align-items:flex-start;flex:1;overflow-y:auto;background:radial-gradient(circle at 0% 53%,rgba(239,14,93,.8) 10%,transparent 45%),radial-gradient(circle at 135% 53%,rgba(2,123,255,.8) 10%,transparent 95%),radial-gradient(circle at 50% 80%,rgba(14,239,95,.8) 40%,transparent 95%)}main .main-container .bottom-content .page-content{flex:2;overflow-y:auto;align-self:stretch;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;background:#000c;padding:5px}main .main-container .bottom-content .page-content .inner{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;padding:10px 10px 40px;background:#111;align-self:stretch}main .main-container .bottom-content .page-content .inner h1,main .main-container .bottom-content .page-content .inner h2,main .main-container .bottom-content .page-content .inner h3,main .main-container .bottom-content .page-content .inner h4,main .main-container .bottom-content .page-content .inner h5,main .main-container .bottom-content .page-content .inner h6{color:#ddd}main .main-container .bottom-content .page-content .inner p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;color:#666}main .main-container .bottom-content .page-panel{flex:1;overflow-y:auto;align-self:stretch;background:#111;border-top:none}main .main-container .bottom-content .page-panel.left-panel{border-right:1px solid #222;border-left:none}main .main-container .bottom-content .page-panel.left-panel.explr-explorer{flex:.5;overflow-y:auto;padding:0;background:#111;box-shadow:1px 1px .5px .5px inset #0003;max-width:250px}main .main-container .bottom-content .page-panel.right-panel{border-left:1px solid #222;border-right:none}.invisible{visibility:hidden!important}.hidden{display:none!important}.tac{text-align:center!important}.tar{text-align:right!important}a{text-decoration:none}.normal{font-weight:400!important}.bold{font-weight:700!important}.col{display:flex;flex:1;flex-direction:column;align-self:stretch}main .context-bar{padding:10px;position:sticky;top:0;z-index:1000;max-height:80px;border-bottom:1px solid #222;display:flex;flex-direction:row;align-items:center}main .context-bar .context-menu{flex:1}main .context-bar .context-menu .inner{display:flex}main .context-bar .context-menu .inner ul.pills{margin:0}main .context-bar .context-divider{width:1px;height:100%;background:#222;margin-left:20px;margin-right:20px}main .context-bar .contex-tail{margin-right:20px}main .context-bar .contex-tail .btn{margin-right:0}main .context-bar .context-user{display:flex;margin-right:20px}main .context-bar .context-user .trigger{color:#fff}main .context-bar .context-user .trigger .avatar{width:32px;height:32px;border-radius:4px;background:#aaa;margin-right:10px;display:flex;flex-direction:row;justify-content:center;align-items:center;text-align:center;font-weight:700;font-size:14px;border:1px solid #444;color:#000}main .context-bar .context-user .trigger i{margin-top:-5px;margin-left:10px}menu{width:300px;background:#111;overflow-y:auto;overflow-x:visible;padding:20px;z-index:2000;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;border-right:1px solid #222;min-width:64px}menu h1.logo{margin:40px 0 0 10px;align-self:stretch;display:flex}menu h1.logo a{text-align:center;text-shadow:0px 0 0 rgb(255,255,255),0px 2px 0 #444,0 0px 0 rgb(14,239,95),-0px 0 0 rgb(2,123,255),0 -0px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent;flex:1;font-family:Sixtyfour,Work Sans,Arial,"sans-serif";align-self:stretch;padding-right:3px;font-size:20px;text-transform:uppercase;transition:all .55s cubic-bezier(.19,1,.22,1);display:flex;flex-direction:row;justify-content:center;align-items:center;position:relative;color:#fff}menu h1.logo a img{flex-shrink:0;width:30px;margin-right:10px;position:absolute;left:5px;transition:all .55s cubic-bezier(.19,1,.22,1)}menu h1.logo a img.after{opacity:0}menu:hover h1.logo a{text-align:center;text-shadow:3px 0 0 rgb(255,255,255),3px 2px 0 #444,0 3px 0 rgb(14,239,95),-3px 0 0 rgb(2,123,255),0 -3px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}menu:hover h1.logo a img.before{opacity:0}menu:hover h1.logo a img.after{animation-duration:.2s;animation-name:logotouch}menu nav{display:flex;align-self:stretch;flex:1}menu nav ul{margin:60px 0 20px;flex:1;align-self:flex-start;display:flex;flex-direction:column;list-style:none}menu nav ul li{align-self:stretch;overflow:hidden;position:relative;transition:all .55s cubic-bezier(.19,1,.22,1);margin:10px 0;border-radius:4px}menu nav ul li a{color:#ffffffe6;font-size:16px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1;padding-top:5px;padding-bottom:5px;padding-left:10px}menu nav ul li a i{color:#fff;opacity:.2;background:transparent;display:flex;justify-content:center;align-items:center;align-self:stretch;padding:10px;width:40px;border-radius:4px;text-align:center;margin-right:20px}menu nav ul li:after{background:#fff;content:"";height:195px;left:-200px;opacity:.2;position:absolute;top:-50px;transform:rotate(35deg);transition:all .55s cubic-bezier(.19,1,.22,1);width:50px;z-index:-2;cursor:pointer}menu nav ul li.active a{color:#027bff;font-weight:700}menu nav ul li.active a i{opacity:1;color:#fff;background:#ffffffe6;background:#027bff}menu nav ul li:hover{background:#027bff}menu nav ul li:hover:after{z-index:2;left:120%;transition:all .55s cubic-bezier(.19,1,.22,1)}menu nav ul li:hover a{color:#fff;font-weight:700}menu nav ul li:hover a i{color:#fff;opacity:1}menu footer{background:#ffffff03;padding:20px 0;display:flex;flex-direction:row;align-self:stretch;text-align:center;justify-content:center}menu footer p{color:#444}menu footer p.version a{color:#777;font-weight:700}.dropdown{position:relative;display:flex;align-self:stretch}.dropdown.dropdown-show ul.dropdown-menu{display:flex;flex-direction:column}.dropdown .trigger{cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1}.dropdown ul.dropdown-menu{position:absolute;top:100%;left:0;display:none;background-color:#222;box-shadow:0 8px 16px #0003;z-index:1000;list-style-type:none;margin:0;overflow:hidden;border-radius:4px}.dropdown ul.dropdown-menu li{padding:8px 16px;cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;transition:all .55s cubic-bezier(.19,1,.22,1)}.dropdown ul.dropdown-menu li.danger:hover{background-color:#ef0e5d}.dropdown ul.dropdown-menu li:hover{background-color:#027bff}.dropdown ul.dropdown-menu li a{padding:8px 16px 8px 8px;color:#fff;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch}.dropdown ul.dropdown-menu li a i{margin-right:15px}@keyframes logotouch{0%{opacity:0}50%{opacity:1}to{opacity:0;left:27px}}@keyframes shakednd{0%{transform:rotate(-2deg)}to{transform:rotate(2deg)}}button,.btn{position:relative;padding:10px 10px 8px;font-size:14px;color:#fff;cursor:pointer;border:none;border-radius:4px;background:#027bff;box-shadow:0 2px #004a9b;font-weight:700;letter-spacing:-.5px;margin-top:-2px;min-width:38px;min-height:34px;text-align:center;justify-content:center}button i.icon-left,.btn i.icon-left{margin-right:5px}button:hover,.btn:hover{box-shadow:0 2px 0 1px #004a9b inset;color:#fffc}button:focus,.btn:focus{background:#004a9b;color:#ffffff80;box-shadow:none}button.btn-pixel,.btn.btn-pixel{background:#ccc;border:1px solid transparent;transition:all .55s cubic-bezier(.19,1,.22,1);text-transform:uppercase;font-size:12px;box-shadow:4px 0 #fff,0 4px #0eef5f,-4px 0 #027bff,0 -4px #ef0e5d;color:#222;overflow:hidden}button.btn-pixel:hover,.btn.btn-pixel:hover{box-shadow:6px 0 #fff,0 6px #0eef5f,-6px 0 #027bff,0 -6px #ef0e5d;text-align:center;text-shadow:4px 0 0 rgb(255,255,255),4px 2px 0 #444,0 4px 0 rgb(14,239,95),-4px 0 0 rgb(2,123,255),0 -4px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}button.btn-neutral,.btn.btn-neutral{color:#fff;background:#555;box-shadow:0 2px #3c3c3c;border:1px solid transparent}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #222 inset;background:#3c3c3c}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#222;border:1px solid #AAA}button .btn-wire-neutral,.btn .btn-wire-neutral{background:transparent;border:2px solid #555;color:#fffc;box-shadow:none}button .btn-wire-neutral:hover,.btn .btn-wire-neutral:hover{background:#5555550d;border-color:#3c3c3c;color:#3c3c3c;box-shadow:none}button .btn-wire-neutral:focus,.btn .btn-wire-neutral:focus{border-color:#222;background:transparent}button.btn-naked,.btn.btn-naked{background:transparent;box-shadow:none;border:1px solid transparent;color:#fff}button.btn-naked:hover,.btn.btn-naked:hover{box-shadow:0 2px 0 1px #222 inset;background:#3c3c3c;color:#fff}button.btn-naked:focus,.btn.btn-naked:focus{background:#222;border:1px solid #AAA}button i.main,.btn i.main{font-size:18px}button sup,button sub,.btn sup,.btn sub{position:absolute;top:-4px;right:-4px;background:#777;border-bottom:2px solid #555;color:red;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}button sup i,button sub i,.btn sup i,.btn sub i{color:#fff;font-size:10px}button.btn-double-icon,.btn.btn-double-icon{margin-right:5px}button.disabled,.btn.disabled{cursor:default}.alert{padding:20px;align-self:stretch;display:flex;flex-direction:row;justify-content:center;align-items:center;border-radius:4px}.alert i{margin-right:13px}.alert a{color:inherit;margin-left:4px;margin-right:4px;text-decoration:underline}ul.explr-tree{height:100%!important}ul.explr-tree ul{padding-top:0!important}ul.explr-tree li{position:relative}ul.explr-tree li span{color:#555;font-size:13px;padding-left:5px;cursor:pointer}ul.explr-tree li span.explr-plus,ul.explr-tree li span.explr-minus{z-index:1}ul.explr-tree li span.explr-plus:hover,ul.explr-tree li span.explr-minus:hover{color:#aaa}ul.explr-tree li i.main{font-size:14px}ul.explr-tree li sup,ul.explr-tree li sub{position:absolute;top:0;left:5px;background:#777;border-bottom:2px solid #555;border-radius:4px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-tree li sup i,ul.explr-tree li sub i{color:#fff;font-size:4px!important;margin-bottom:0}ul.explr-tree li a{color:#fff;padding-right:80px;margin-top:2px}ul.explr-tree li a:hover{color:#fff}ul.explr-tree li a.active{background:#ffffff1a;border-radius:4px;font-weight:700;text-decoration:underline;margin-left:35px;padding-left:5px;margin-right:10px}.explr-multiselection-actions,.explr-selection-actions{display:none;flex-direction:row;justify-content:flex-end;align-items:center;flex:1}.explr-multiselection-actions button,.explr-selection-actions button{display:none}body.explr-selection-actionable .explr-selection-actions,body.explr-selection-actionable.explr-selection-folder .explr-selection-actions button.explr-selection-folder,body.explr-selection-actionable.explr-selection-entity .explr-selection-actions button.explr-selection-entity,body.explr-multiselection-actionable .explr-multiselection-actions,body.explr-multiselection-actionable.explr-multiselection-folder .explr-multiselection-actions button.explr-multiselection-folder,body.explr-multiselection-actionable.explr-multiselection-entity .explr-multiselection-actions button.explr-multiselection-entity{display:flex}.selectable-zone{flex:1;align-self:stretch;border:1px solid transparent}ul.explr-dirview{display:flex;flex-direction:row;flex-wrap:wrap}ul.explr-dirview li{display:flex;flex-direction:column;justify-content:flex-start;align-items:center;flex-shrink:0;margin:10px;min-width:100px;min-height:130px;padding-top:5px;border:1px solid transparent;border-radius:4px}ul.explr-dirview li.renaming a span{display:none}ul.explr-dirview li.renaming a form{display:block}ul.explr-dirview li.highlight-drop{border:1px dotted rgba(2,123,255,.4);background:#027bff4d}ul.explr-dirview li.highlight-clicked{border:1px dotted rgba(255,255,255,.2);background:#ffffff1a}ul.explr-dirview li a{color:#bbb;text-decoration:none;flex:1;text-align:center;font-size:12px;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;max-width:84px;min-width:84px;position:relative;word-break:break-all}ul.explr-dirview li a.with-thumbnail .img-holder{width:64px;height:64px;background:#000;border-radius:8px;display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden;margin-bottom:12px}ul.explr-dirview li a.with-thumbnail .img-holder img{max-height:100%;max-width:100%}ul.explr-dirview li a.with-thumbnail i{font-size:24px;position:absolute;top:-4px;left:-4px;text-shadow:0 .5px .5px #777}ul.explr-dirview li a i{font-size:64px;margin-bottom:12px;border-radius:8px}ul.explr-dirview li a sup,ul.explr-dirview li a sub{position:absolute;top:-2px;right:0;background:#777;border-bottom:2px solid #555;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-dirview li a sup i,ul.explr-dirview li a sub i{color:#fff;font-size:10px;margin-bottom:0}ul.explr-dirview li a input{width:100%;padding:0 3px}ul.explr-dirview li a input:focus{outline:none}ul.explr-dirview li a:hover{opacity:.8}ul.explr-dirview li a form{display:none}ul.explr-dirview li.new-folder a{color:#027bff}ul.explr-dirview li.new-folder a form{display:block}ul.explr-dirview .ui-draggable-dragging{z-index:20}ul.explr-dirview .ui-draggable-dragging a{opacity:1!important}.modal-explr-picker h2{margin-top:0}.modal-explr-picker .explr-tree{width:400px;max-width:400px;max-height:300px;overflow:auto;background:#222;padding:10px 20px;border-radius:4px;margin-top:15px}.content-explr-picker{cursor:pointer}.selection-rectangle{position:absolute;border:1px solid rgb(0,153,255);background-color:#0099ff1a;pointer-events:none;z-index:1000}ul.pills{background:#222;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #0003;border:1px solid #222;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0}ul.pills li.divider{margin:0 20px;width:1px;height:100%;background:#333}ul.pills li:hover a{opacity:.9}ul.pills li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#fff;overflow:hidden;padding-right:30px;text-align:center;background:#0003;margin-right:5px;transition:all .25s cubic-bezier(.19,1,.22,1)}ul.pills li a span{display:flex;justify-content:center;align-items:center;margin-right:20px;height:42px;background:#0003;width:42px}ul.pills li.active a{color:#333;background:#ccc;font-weight:700}ul.pills li:hover a{color:#fff;background:#027bff}ul.pills li:last-child a{margin-right:0}.breadcrumb-container{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;border-bottom:1px solid #222;background:transparent;padding:10px}.breadcrumb-container ul.breadcrumb{background:#222;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #0003;border:1px solid #222;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0;overflow-x:auto;max-width:65vw;white-space:nowrap}.breadcrumb-container ul.breadcrumb li{display:inline-block}.breadcrumb-container ul.breadcrumb li.divider{margin:0 5px}.breadcrumb-container ul.breadcrumb li.divider i{color:#aaa}.breadcrumb-container ul.breadcrumb li span,.breadcrumb-container ul.breadcrumb li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#fff;text-align:center;padding:0 3px}.breadcrumb-container ul.breadcrumb li span i,.breadcrumb-container ul.breadcrumb li a i{margin-right:5px}.breadcrumb-container ul.breadcrumb li:hover a{color:#fff;background:#027bff}.breadcrumb-container ul.breadcrumb li:last-child a{margin-right:0}.pickers,.modals{position:fixed;background:#0006;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:10000}.pickers.pickers .modals-outer .modals-inner .modal h2,.modals.pickers .modals-outer .modals-inner .modal h2{font-size:14px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.pickers .modals-outer,.modals .modals-outer{min-width:464px;max-width:464px;display:flex;flex-direction:column;overflow:auto;padding-bottom:2px}.pickers .modals-outer .modals-inner,.modals .modals-outer .modals-inner{background:#111;border-radius:10px;color:#333;padding:40px;box-shadow:0 2px #222;border:1px solid #222}.pickers .modals-outer .modals-inner .modal h2,.modals .modals-outer .modals-inner .modal h2{padding:0;margin:0 0 30px;font-weight:400;color:#999}.pickers .modals-outer .modals-inner .modal h3,.modals .modals-outer .modals-inner .modal h3{align-self:stretch;margin:0 0 10px;font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#fff;padding-bottom:10px;text-decoration:none}.pickers .modals-outer .modals-inner .modal h3.divide,.modals .modals-outer .modals-inner .modal h3.divide{border-top:1px solid #222;margin-top:20px;padding-top:20px}.toast{visibility:hidden;min-width:250px;margin-left:-125px;background-color:#333;color:#fff;text-align:center;border-radius:4px;padding:16px;position:fixed;z-index:1;left:50%;bottom:30px;font-size:17px;border:2px dashed #222}.toast.show{visibility:visible;-webkit-animation:fadein .5s,fadeout .5s 2.5s;animation:fadein .5s,fadeout .5s 2.5s}@-webkit-keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@-webkit-keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}@keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}body.dragover .shakeondrag{animation:shakednd .1s linear alternate infinite}.btn-super-upload-busy,.btn-super-upload{display:flex;flex-direction:row;justify-content:center;align-items:center;margin-left:10px;position:relative}.btn-super-upload-busy.btn-super-upload-busy,.btn-super-upload.btn-super-upload-busy{border:none!important}.btn-super-upload-busy .unprogress,.btn-super-upload .unprogress{display:block}.btn-super-upload-busy .progress,.btn-super-upload .progress{display:none;width:200px;height:10px;background:#666;border-radius:4px;flex-direction:row;justify-content:flex-start;align-items:center}.btn-super-upload-busy .progress .progress-bar,.btn-super-upload .progress .progress-bar{border-radius:4px;background-color:#027bff;height:100%}.btn-super-upload-busy .progress .percent,.btn-super-upload .progress .percent{display:flex;justify-content:center;align-items:center;position:absolute;left:0;right:0;bottom:0;top:2px;font-size:15px;color:#fff;text-shadow:0 0 2px rgb(0,0,0)}.btn-super-upload-busy.uploading .progress,.btn-super-upload.uploading .progress{display:block}.btn-super-upload-busy.uploading .unprogress,.btn-super-upload.uploading .unprogress{display:none}.panes{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.panes .pane-section,.panes tbody{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:1px 1px 28px;background:#111;border-radius:4px;border:4px solid rgba(255,255,255,.05)}.panes .pane-section:hover,.panes tbody:hover,.panes .pane-section:hover tr.title-item,.panes tbody:hover tr.title-item{border-color:#027bff0d}.panes .pane-section .pane-item,.panes .pane-section tr,.panes tbody .pane-item,.panes tbody tr{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:8px 18px;background:#222}.panes .pane-section .pane-item:nth-child(odd),.panes .pane-section tr:nth-child(odd),.panes tbody .pane-item:nth-child(odd),.panes tbody tr:nth-child(odd){background-color:#111}.panes .pane-section .pane-item:nth-child(odd) td.description,.panes .pane-section tr:nth-child(odd) td.description,.panes tbody .pane-item:nth-child(odd) td.description,.panes tbody tr:nth-child(odd) td.description{color:#ffffffb3}.panes .pane-section .pane-item:nth-child(2n),.panes .pane-section tr:nth-child(2n),.panes tbody .pane-item:nth-child(2n),.panes tbody tr:nth-child(2n){background-color:#141414}.panes .pane-section .pane-item .pane-cell,.panes .pane-section .pane-item td,.panes .pane-section tr .pane-cell,.panes .pane-section tr td,.panes tbody .pane-item .pane-cell,.panes tbody .pane-item td,.panes tbody tr .pane-cell,.panes tbody tr td{color:#888;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;flex:1;font-size:14px}.panes .pane-section .pane-item .pane-cell.vertical,.panes .pane-section .pane-item td.vertical,.panes .pane-section tr .pane-cell.vertical,.panes .pane-section tr td.vertical,.panes tbody .pane-item .pane-cell.vertical,.panes tbody .pane-item td.vertical,.panes tbody tr .pane-cell.vertical,.panes tbody tr td.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}.panes .pane-section .pane-item .pane-cell.description,.panes .pane-section .pane-item td.description,.panes .pane-section tr .pane-cell.description,.panes .pane-section tr td.description,.panes tbody .pane-item .pane-cell.description,.panes tbody .pane-item td.description,.panes tbody tr .pane-cell.description,.panes tbody tr td.description{align-self:stretch}.panes .pane-section .pane-item .pane-cell.value,.panes .pane-section .pane-item td.value,.panes .pane-section tr .pane-cell.value,.panes .pane-section tr td.value,.panes tbody .pane-item .pane-cell.value,.panes tbody .pane-item td.value,.panes tbody tr .pane-cell.value,.panes tbody tr td.value{flex:0;margin-left:20px;word-break:break-all;flex-basis:auto}.panes .pane-section .pane-item .pane-cell.value i.icon-legend,.panes .pane-section .pane-item td.value i.icon-legend,.panes .pane-section tr .pane-cell.value i.icon-legend,.panes .pane-section tr td.value i.icon-legend,.panes tbody .pane-item .pane-cell.value i.icon-legend,.panes tbody .pane-item td.value i.icon-legend,.panes tbody tr .pane-cell.value i.icon-legend,.panes tbody tr td.value i.icon-legend{font-size:10px;margin-right:10px}.panes .pane-section .pane-item .pane-cell.value i.icon-value,.panes .pane-section .pane-item td.value i.icon-value,.panes .pane-section tr .pane-cell.value i.icon-value,.panes .pane-section tr td.value i.icon-value,.panes tbody .pane-item .pane-cell.value i.icon-value,.panes tbody .pane-item td.value i.icon-value,.panes tbody tr .pane-cell.value i.icon-value,.panes tbody tr td.value i.icon-value{padding:2px 2px 1px;border-radius:2px;width:16px;text-align:center}.panes .pane-section .pane-item.title-item,.panes .pane-section tr.title-item,.panes tbody .pane-item.title-item,.panes tbody tr.title-item{border-bottom:1px solid #222}.panes .pane-section .pane-item.title-item td,.panes .pane-section tr.title-item td,.panes tbody .pane-item.title-item td,.panes tbody tr.title-item td{color:#027bffe6;font-size:16px;font-weight:700}.panes .pane-section .pane-item.title-item td i,.panes .pane-section tr.title-item td i,.panes tbody .pane-item.title-item td i,.panes tbody tr.title-item td i{margin-right:10px}.panes .pane-section .pane-item.title-item td .more,.panes .pane-section tr.title-item td .more,.panes tbody .pane-item.title-item td .more,.panes tbody tr.title-item td .more{flex:1;text-align:right;font-size:12px;color:#888;font-weight:400;font-style:italic}.panes .pane-section .pane-item.variable-item,.panes .pane-section tr.variable-item,.panes tbody .pane-item.variable-item,.panes tbody tr.variable-item{cursor:pointer}.panes .pane-section .pane-item.variable-item:hover,.panes .pane-section tr.variable-item:hover,.panes tbody .pane-item.variable-item:hover,.panes tbody tr.variable-item:hover{background-color:#027bff0d}.panes .pane-section .pane-item.variable-item:hover td,.panes .pane-section tr.variable-item:hover td,.panes tbody .pane-item.variable-item:hover td,.panes tbody tr.variable-item:hover td{font-weight:700;color:#fff}.panes .pane-section .pane-item.variable-item:hover td i.icon-legend,.panes .pane-section tr.variable-item:hover td i.icon-legend,.panes tbody .pane-item.variable-item:hover td i.icon-legend,.panes tbody tr.variable-item:hover td i.icon-legend{color:#fff}.panes .pane-section .pane-item.variable-item:hover td span,.panes .pane-section .pane-item.variable-item:hover td i.icon-value,.panes .pane-section tr.variable-item:hover td span,.panes .pane-section tr.variable-item:hover td i.icon-value,.panes tbody .pane-item.variable-item:hover td span,.panes tbody .pane-item.variable-item:hover td i.icon-value,.panes tbody tr.variable-item:hover td span,.panes tbody tr.variable-item:hover td i.icon-value{background-color:#ffffff4d;color:#fff}.panes .pane-section .pane-item.variable-item:hover td.description,.panes .pane-section tr.variable-item:hover td.description,.panes tbody .pane-item.variable-item:hover td.description,.panes tbody tr.variable-item:hover td.description{color:#027bff}.tiles{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;align-self:stretch}.tiles .tiles-inner{display:flex;flex:1;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;align-self:stretch;padding:2px}.tiles .tiles-inner .tile-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#fff;margin:1px;padding:15px 10px 15px 15px;border-radius:4px;border-bottom:1px solid transparent}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-left:4px solid rgb(2,123,255);border-radius:4px;border-bottom:2px solid #171717;background:#222;color:#027bff}.tiles .tiles-inner .tile-item:hover:hover,.tiles .tiles-inner .tile-item.active:hover{opacity:1}.tiles .tiles-inner .tile-item:hover.disabled,.tiles .tiles-inner .tile-item.active.disabled{border-left-color:#444;color:#fff}.tiles .tiles-inner .tile-item:hover.starred,.tiles .tiles-inner .tile-item.active.starred{border-left-color:#e5a123;color:#e5a123}.tiles .tiles-inner .tile-item:hover.starred .tile-tail .head-icon i,.tiles .tiles-inner .tile-item.active.starred .tile-tail .head-icon i{opacity:1;color:#e5a123;font-size:8px}.tiles .tiles-inner .tile-item.starred .tile-tail .head-icon i{font-size:8px;color:#e5a123}.tiles .tiles-inner .tile-item.disabled .tile-body{opacity:.3}.tiles .tiles-inner .tile-item.disabled .tile-tail .head-icon i{color:#444;opacity:.4}.tiles .tiles-inner .tile-item .tile-tail{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin-left:10px;min-width:10px}.tiles .tiles-inner .tile-item .tile-tail .head-icon{flex:1;display:flex;text-align:right;flex-direction:row;justify-content:center;align-items:center;align-self:stretch}.tiles .tiles-inner .tile-item .tile-tail .head-icon i{font-size:6px;display:flex}.tiles .tiles-inner .tile-item .tile-tail .status-icons{display:flex;flex-direction:row;justify-content:flex-end;align-items:center}.tiles .tiles-inner .tile-item .tile-tail .status-icons i{font-size:16px;margin-left:10px}.tiles .tiles-inner .tile-item .tile-body{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;font-size:15px;font-weight:400;letter-spacing:.8px;line-height:22px;margin:0;flex-wrap:nowrap}.tiles .tiles-inner .tile-item .tile-body i{font-size:8px;margin-right:5px}.tiles .tiles-inner .tile-item .tile-metrics{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;margin:0}.tiles .tiles-inner .tile-item .tile-metrics .foot-span span,.tiles .tiles-inner .tile-item .tile-metrics .foot-span{opacity:.8;font-size:13px;font-family:Courier New}span.empty{background:#e5a1234d;color:#e5a123;text-transform:lowercase;border-radius:2px;padding:2px 4px;font-weight:700}.inner-empty{display:flex;flex:1;flex-direction:column;align-self:stretch;justify-content:center;align-items:center}.inner-empty i{font-size:90px;opacity:.3;text-shadow:0 -1px #333,0 0px .5px #444}.toggle{position:relative;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.toggle input{display:none}.toggle input:checked+label{background:#027bff;border:1px solid rgba(255,255,255,.1);box-shadow:0 2px 2px #222 inset}.toggle input:checked+label:after{content:"";display:block;border-radius:50%;margin-left:21px;width:18px;height:18px;transition:.2s;background:#9bcbff;box-shadow:0 2px #0063ce}.toggle label{width:44px;height:26px;border-radius:15px;background:#222;cursor:pointer;border:1px solid rgba(255,255,255,.1);box-shadow:0 2px 2px #111 inset}.toggle label:after{content:"";display:block;border-radius:50%;width:18px;height:18px;margin:3px;background:#777;box-shadow:0 2px #555555e6;transition:.2s}.form-holder{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.form-holder form{max-width:434px}form{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}form .alert{padding:8px 15px;font-size:14px;margin:0 0 25px}.from-group-condition{flex-direction:column;align-self:stretch;justify-content:flex-start;align-items:flex-start;display:flex}.form-group{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;width:100%;flex:1;margin-bottom:20px}.form-group label{flex:1;font-size:12px;line-height:18px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.form-group label.btn-upload{color:#fff;font-size:14px;flex:0;flex-basis:auto;margin-top:5px}.form-group label.btn-upload input[type=file]{display:none}.form-group label.btn-upload input[type=text]{margin-bottom:2px;margin-left:10px}.form-group label.btn-upload span.btn{padding-right:20px}.form-group label.btn-upload i{margin-left:3px;margin-right:10px}.form-group .widget{margin-top:10px;align-self:stretch;display:flex;flex-direction:row}.form-group .widget.vertical{flex-direction:column}.form-group .widget.vertical select,.form-group .widget.vertical input{align-self:stretch}.form-group .widget.vertical select:first-child,.form-group .widget.vertical input:first-child{margin-bottom:10px}.form-group .widget .btn{margin-left:10px}.form-group .widget.widget-unit select,.form-group .widget.widget-unit input{flex-grow:0;background:none;box-shadow:none;border:none;border-bottom:1px solid #333;border-radius:0;max-width:80px;padding-left:0;color:#fff;text-align:center}.form-group .widget.widget-unit span{font-size:12px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-left:5px;color:#555}.form-group .widget div{color:#ffffffb3;font-size:14px}.form-group .widget select,.form-group .widget input,.form-group .widget textarea{outline:none;padding:8px 0 5px 8px;border-radius:2px;border:1px solid rgba(255,255,255,.05);flex:1;background:#555;box-shadow:0 2px 1px #444,0 4px 2px #333 inset;color:#ddd;font-size:14px}.form-group .widget select.input-naked,.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.input-naked,.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.input-naked,.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{color:#555;background:none;box-shadow:none;border:none;border-bottom:1px solid #333;border-radius:0}.form-group .widget select.input-naked,.form-group .widget input.input-naked,.form-group .widget textarea.input-naked{padding-left:0;color:#bbb}.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{border:none;background:#000;border-radius:4px;padding-left:10px;padding-right:10px}.form-group.tab-select{border-bottom:1px solid #444;display:flex;flex-direction:row;position:relative;height:48px;padding:48px 0 0;flex:0;flex-basis:auto}.form-group.tab-select .widget{height:49px;margin-top:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;position:absolute;top:0;left:0;border-bottom:2px solid rgb(2,123,255);color:#027bff}.form-group.tab-select .widget select{border:none;background:none;box-shadow:none;padding:10px 35px 10px 10px;margin:0;color:inherit;appearance:none;-moz-appearance:none;-webkit-appearance:none;text-align:left;font-weight:700;cursor:pointer;border-radius:4px 4px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;z-index:2}.form-group.tab-select .widget i{margin-left:10px;margin-right:0}.form-group.tab-select .widget i.triangle{margin-top:-4px;margin-left:0;position:absolute;right:10px}.form-group.form-group-horizontal{margin:10px 0 20px;flex-direction:row;justify-content:flex-start;align-items:center}.form-group.form-group-horizontal .widget{margin:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.actions{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin:20px 0 0}.actions.actions-intermediate{margin:0}.actions button{margin-left:25px}.actions.actions-left{justify-content:flex-start}.actions.actions-left .btn{margin-left:0;margin-right:25px}.actions.actions-right{justify-content:flex-end}.actions.actions-right .btn{margin-left:10px;margin-right:0}.actions.actions-center{justify-content:center}.actions.actions-center .btn{margin-left:0;margin-right:0}.view-content-list main .main-container .page-content .inner{padding-bottom:10px}.view-content-list main .main-container .content-object-input{margin-bottom:6px}.view-content-list.dragover main .main-container .inner .dropzone{border-radius:4px;background:#ffffff1a;border:1px dashed rgba(255,255,255,.5)}.view-content-edit main .main-container .bottom-content .page-content{flex:1}.view-content-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px;flex:1}.view-content-edit main .main-container .bottom-content .page-panel.right-panel{flex:2;align-self:stretch;display:flex;flex-direction:column;overflow:hidden;justify-content:flex-start;align-items:center;padding:20px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3{color:#fff;padding:10px 10px 10px 0;margin-bottom:20px;font-size:16px;align-self:stretch;margin-left:-8px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 span{border-width:1px;border-style:solid;border-radius:4px;padding:4px 10px;margin-left:5px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 i{font-size:16px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper{display:flex;flex-direction:column;width:100%;position:relative;padding-top:56.25%;overflow:hidden;border-radius:4px;outline:4px solid rgba(255,255,255,.1)}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.view-logs-list main .main-container .bottom-content .page-content .inner{padding-top:8px;padding-bottom:8px}.view-logs-list main .main-container .bottom-content .page-content .logs{flex:1;display:flex;flex-direction:column;align-self:stretch}.view-logs-list main .main-container .bottom-content .page-content .logs pre{flex:1;background:#000000e6;border:1px solid rgba(85,85,85,.5);border-radius:4px;font-family:monospace;color:#f2f2f2;padding:20px;overflow:auto;align-self:stretch}.view-node-player-edit main .main-container .bottom-content .page-content{flex:1}.view-node-player-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px}.view-player-group-list main .main-container .players-holder ul.players{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #222;border-radius:4px;padding:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .head{display:flex;flex-direction:column;justify-content:center;align-items:center;color:#999;font-size:10px;padding:10px;cursor:default}.view-player-group-list main .main-container .players-holder ul.players li.player-item:hover .infos .title{color:#fff}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px;flex:1;max-width:180px;background:#000;border:1px solid #333;border-radius:4px;padding:3px 7px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .title{font-size:13px;color:#aaa;display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body{display:block;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#1b1b1b;padding:10px;align-self:stretch;flex:1;border-radius:4px;color:#ccc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px;font-size:12px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body span{opacity:.5;margin-right:7px;font-size:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a{color:#fff}.view-playlist-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.view-playlist-list main .main-container .modal-playlist-qrcode h2{text-align:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic{text-align:center;display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic img{border:4px solid #555;border-radius:4px}.view-playlist-list main .main-container .modal-slide h2{font-size:20px}.view-playlist-list main .main-container .modal-slide input[disabled]{color:#aaa}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select{margin-right:5px}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input{font-size:12px;max-width:50%}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input.datetimepicker{margin-left:5px;padding-left:0}.view-playlist-list main .main-container .bottom-content .page-content{flex:1}.view-playlist-list main .main-container .bottom-content .page-content.with-right-panel{flex:.5}.view-playlist-list main .main-container .bottom-content .page-content .inner{padding:0}.view-playlist-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#ddd;text-decoration:none;margin:0 0 20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder{margin:20px 20px 20px 10px;flex:1}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder{margin:20px 0 0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder form{max-width:initial}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder .form-group{flex-grow:0;margin-bottom:5px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder{position:relative}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .hover-only{display:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#fff;padding-bottom:10px;text-decoration:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4.divide{border-top:1px solid #222;margin-top:20px;padding-top:20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic{margin-top:10px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic img{border:1px dashed #555;padding:5px;border-radius:4px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview{background:#000;border:1px solid rgba(255,255,255,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview iframe{flex:1;align-self:stretch}.view-playlist-list main .main-container .bottom-content .page-content .inner .slides-holder{align-self:stretch;border-right:1px solid #222;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-playlist-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-player-group-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#666}.view-player-group-list main .main-container .bottom-content .page-content{flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner{padding:0}.view-player-group-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#ddd;text-decoration:none;margin:0 0 20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder{margin:20px 20px 20px 10px;flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder{margin:20px 0 0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder form{max-width:initial}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder .form-group{flex-grow:0;margin-bottom:15px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder{position:relative}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .hover-only{display:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#fff;padding-bottom:10px;text-decoration:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4.divide{border-top:1px solid #222;margin-top:20px;padding-top:20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview{background:#000;border:1px solid rgba(255,255,255,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview iframe{flex:1;align-self:stretch}.view-player-group-list main .main-container .bottom-content .page-content .inner .players-holder{align-self:stretch;border-right:1px solid #222;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-player-group-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-playlist-list main .main-container .page-content .inner h3.divide{margin-top:50px}.view-playlist-list main .main-container .slides-holder ul.slides{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #222;border-radius:4px;padding:10px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .title{color:#333}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .type i{color:#333!important}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .body{opacity:0}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort{display:flex;flex-direction:column;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a{color:#999;font-size:10px;padding:10px;cursor:move}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a:hover{color:#027bff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px;flex:1;max-width:120px;background:#000;border:1px solid #333;border-radius:4px;padding:3px 7px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos:hover .title{color:#fff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .title{display:block;word-break:break-all;font-size:13px;color:#aaa;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#aaa;font-size:12px;margin-right:5px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#1b1b1b;padding:10px;align-self:stretch;flex:1;border-radius:4px;font-size:13px;color:#fff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;max-width:315px;overflow-x:auto;white-space:nowrap}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-bottom:8px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end{display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .prefix,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .prefix{margin-left:5px;margin-right:5px;font-size:12px;color:#ddd}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .cron-description,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .cron-description{display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;margin-left:5px;font-size:10px;opacity:.5}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a{color:#fff}.view-plugins-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-settings-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list .reboot{color:#ccc}.view-login main .main-container{position:relative}.view-login main .main-container .alert{position:absolute;top:0;left:0;right:0}.view-login main .main-container .login-content{display:flex;flex:1;flex-direction:column;justify-content:center;align-items:center}.view-login main .main-container .login-content .form-holder{width:400px;display:flex;justify-content:center;align-items:center;align-self:stretch;margin-left:auto;margin-right:auto}.view-login main .main-container .login-content .form-holder .card{display:flex;justify-content:center;align-items:center;align-self:stretch;border-radius:6px;padding:50px;color:#333}.view-login main .main-container .login-content .form-holder .card form{padding:0;flex:1;display:flex;justify-content:center;align-items:center;align-self:stretch}.view-login main .main-container .login-content .form-holder .card form .actions{margin-top:10px}.view-login main .main-container .login-content .form-holder .card form .actions .btn{padding-left:20px;padding-right:20px}.view-auth-user-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item.disabled .tile-body{opacity:.3;text-decoration-line:line-through}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-tail .btn{margin-left:10px}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-tail .btn:first-child{margin-left:0}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics{flex:1;flex-direction:row;justify-content:flex-start;align-items:center}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics .widget,.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics .form-group{margin:0}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics label{flex-grow:0}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics input{margin-left:10px;margin-right:10px;max-width:320px} diff --git a/data/www/css/compiled/main-light-mode.css b/data/www/css/compiled/main-light-mode.css index 2407513..83c60ab 100644 --- a/data/www/css/compiled/main-light-mode.css +++ b/data/www/css/compiled/main-light-mode.css @@ -1 +1 @@ -.warning{color:#e56723!important}.bg-warning{background-color:#e56723!important}.border-warning{border-color:#e56723!important}.info{color:#027bff!important}.bg-info{background-color:#027bff!important}.border-info{border-color:#027bff!important}.info-alt{color:#075cb7!important}.bg-info-alt{background-color:#075cb7!important}.border-info-alt{border-color:#075cb7!important}.success{color:#11a948!important}.bg-success{background-color:#11a948!important}.border-success{border-color:#11a948!important}.success-alt{color:#11a948!important}.bg-success-alt{background-color:#11a948!important}.border-success-alt{border-color:#11a948!important}.error{color:#ef0e5d!important}.bg-error{background-color:#ef0e5d!important}.border-error{border-color:#ef0e5d!important}.error-alt{color:#c20941!important}.bg-error-alt{background-color:#c20941!important}.border-error-alt{border-color:#c20941!important}.danger{color:#ef0e5d!important}.bg-danger{background-color:#ef0e5d!important}.border-danger{border-color:#ef0e5d!important}.danger-alt{color:#c20941!important}.bg-danger-alt{background-color:#c20941!important}.border-danger-alt{border-color:#c20941!important}.purple{color:#bc48ff!important}.bg-purple{background-color:#bc48ff!important}.border-purple{border-color:#bc48ff!important}.purple-alt{color:#692fbd!important}.bg-purple-alt{background-color:#692fbd!important}.border-purple-alt{border-color:#692fbd!important}.neutral{color:#464646!important}.bg-neutral{background-color:#464646!important}.border-neutral{border-color:#464646!important}.yellow{color:#ffa70a!important}.bg-yellow{background-color:#ffa70a!important}.border-yellow{border-color:#ffa70a!important}.white{color:#fff!important}.bg-white{background-color:#fff!important}.border-white{border-color:#fff!important}.black{color:#000!important}.bg-black{background-color:#000!important}.border-black{border-color:#000!important}.youtube{color:#fd3c01!important}.bg-youtube{background-color:#fd3c01!important}.border-youtube{border-color:#fd3c01!important}.raspbian{color:#b61240!important}.bg-raspbian{background-color:#b61240!important}.border-raspbian{border-color:#b61240!important}.windows{color:#12a7e3!important}.bg-windows{background-color:#12a7e3!important}.border-windows{border-color:#12a7e3!important}.macos{color:#b3bcc2!important}.bg-macos{background-color:#b3bcc2!important}.border-macos{border-color:#b3bcc2!important}.debian{color:#cf084e!important}.bg-debian{background-color:#cf084e!important}.border-debian{border-color:#cf084e!important}.fedora{color:#52a2da!important}.bg-fedora{background-color:#52a2da!important}.border-fedora{border-color:#52a2da!important}.ubuntu{color:#d64514!important}.bg-ubuntu{background-color:#d64514!important}.border-ubuntu{border-color:#d64514!important}.suse{color:#6fb425!important}.bg-suse{background-color:#6fb425!important}.border-suse{border-color:#6fb425!important}.redhat{color:#c60200!important}.bg-redhat{background-color:#c60200!important}.border-redhat{border-color:#c60200!important}.centos{color:#9b4c88!important}.bg-centos{background-color:#9b4c88!important}.border-centos{border-color:#9b4c88!important}.other{color:#ffa70a!important}.bg-other{background-color:#ffa70a!important}.border-other{border-color:#ffa70a!important}button.btn-warning,.btn.btn-warning{background:#e56723;box-shadow:0 2px #913e11}button.btn-warning:hover,.btn.btn-warning:hover{box-shadow:0 2px 0 1px #913e11 inset}button.btn-warning:focus,.btn.btn-warning:focus{background:#913e11}button.btn-wire-warning,.btn.btn-wire-warning{background:transparent;box-shadow:none;border:2px solid rgb(229,103,35);color:#000c}button.btn-wire-warning i.btn-match,.btn.btn-wire-warning i.btn-match{color:#e56723}button.btn-wire-warning:hover,.btn.btn-wire-warning:hover{background:#e567230d;border-color:#be5117;color:#be5117;box-shadow:none}button.btn-wire-warning:focus,.btn.btn-wire-warning:focus{border-color:#913e11;background:transparent}button.btn-info,.btn.btn-info{background:#027bff;box-shadow:0 2px #004a9b}button.btn-info:hover,.btn.btn-info:hover{box-shadow:0 2px 0 1px #004a9b inset}button.btn-info:focus,.btn.btn-info:focus{background:#004a9b}button.btn-wire-info,.btn.btn-wire-info{background:transparent;box-shadow:none;border:2px solid rgb(2,123,255);color:#000c}button.btn-wire-info i.btn-match,.btn.btn-wire-info i.btn-match{color:#027bff}button.btn-wire-info:hover,.btn.btn-wire-info:hover{background:#027bff0d;border-color:#0063ce;color:#0063ce;box-shadow:none}button.btn-wire-info:focus,.btn.btn-wire-info:focus{border-color:#004a9b;background:transparent}button.btn-info-alt,.btn.btn-info-alt{background:#075cb7;box-shadow:0 2px #032b55}button.btn-info-alt:hover,.btn.btn-info-alt:hover{box-shadow:0 2px 0 1px #032b55 inset}button.btn-info-alt:focus,.btn.btn-info-alt:focus{background:#032b55}button.btn-wire-info-alt,.btn.btn-wire-info-alt{background:transparent;box-shadow:none;border:2px solid rgb(7,92,183);color:#000c}button.btn-wire-info-alt i.btn-match,.btn.btn-wire-info-alt i.btn-match{color:#075cb7}button.btn-wire-info-alt:hover,.btn.btn-wire-info-alt:hover{background:#075cb70d;border-color:#054386;color:#054386;box-shadow:none}button.btn-wire-info-alt:focus,.btn.btn-wire-info-alt:focus{border-color:#032b55;background:transparent}button.btn-success,.btn.btn-success{background:#11a948;box-shadow:0 2px #084c21}button.btn-success:hover,.btn.btn-success:hover{box-shadow:0 2px 0 1px #084c21 inset}button.btn-success:focus,.btn.btn-success:focus{background:#084c21}button.btn-wire-success,.btn.btn-wire-success{background:transparent;box-shadow:none;border:2px solid rgb(17,169,72);color:#000c}button.btn-wire-success i.btn-match,.btn.btn-wire-success i.btn-match{color:#11a948}button.btn-wire-success:hover,.btn.btn-wire-success:hover{background:#11a9480d;border-color:#0c7b34;color:#0c7b34;box-shadow:none}button.btn-wire-success:focus,.btn.btn-wire-success:focus{border-color:#084c21;background:transparent}button.btn-success-alt,.btn.btn-success-alt{background:#11a948;box-shadow:0 2px #084c21}button.btn-success-alt:hover,.btn.btn-success-alt:hover{box-shadow:0 2px 0 1px #084c21 inset}button.btn-success-alt:focus,.btn.btn-success-alt:focus{background:#084c21}button.btn-wire-success-alt,.btn.btn-wire-success-alt{background:transparent;box-shadow:none;border:2px solid rgb(17,169,72);color:#000c}button.btn-wire-success-alt i.btn-match,.btn.btn-wire-success-alt i.btn-match{color:#11a948}button.btn-wire-success-alt:hover,.btn.btn-wire-success-alt:hover{background:#11a9480d;border-color:#0c7b34;color:#0c7b34;box-shadow:none}button.btn-wire-success-alt:focus,.btn.btn-wire-success-alt:focus{border-color:#084c21;background:transparent}button.btn-error,.btn.btn-error{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-error:hover,.btn.btn-error:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-error:focus,.btn.btn-error:focus{background:#8f0838}button.btn-wire-error,.btn.btn-wire-error{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#000c}button.btn-wire-error i.btn-match,.btn.btn-wire-error i.btn-match{color:#ef0e5d}button.btn-wire-error:hover,.btn.btn-wire-error:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-error:focus,.btn.btn-wire-error:focus{border-color:#8f0838;background:transparent}button.btn-error-alt,.btn.btn-error-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-error-alt:hover,.btn.btn-error-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-error-alt:focus,.btn.btn-error-alt:focus{background:#610420}button.btn-wire-error-alt,.btn.btn-wire-error-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#000c}button.btn-wire-error-alt i.btn-match,.btn.btn-wire-error-alt i.btn-match{color:#c20941}button.btn-wire-error-alt:hover,.btn.btn-wire-error-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-error-alt:focus,.btn.btn-wire-error-alt:focus{border-color:#610420;background:transparent}button.btn-danger,.btn.btn-danger{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-danger:hover,.btn.btn-danger:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-danger:focus,.btn.btn-danger:focus{background:#8f0838}button.btn-wire-danger,.btn.btn-wire-danger{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#000c}button.btn-wire-danger i.btn-match,.btn.btn-wire-danger i.btn-match{color:#ef0e5d}button.btn-wire-danger:hover,.btn.btn-wire-danger:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-danger:focus,.btn.btn-wire-danger:focus{border-color:#8f0838;background:transparent}button.btn-danger-alt,.btn.btn-danger-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-danger-alt:hover,.btn.btn-danger-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-danger-alt:focus,.btn.btn-danger-alt:focus{background:#610420}button.btn-wire-danger-alt,.btn.btn-wire-danger-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#000c}button.btn-wire-danger-alt i.btn-match,.btn.btn-wire-danger-alt i.btn-match{color:#c20941}button.btn-wire-danger-alt:hover,.btn.btn-wire-danger-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-danger-alt:focus,.btn.btn-wire-danger-alt:focus{border-color:#610420;background:transparent}button.btn-purple,.btn.btn-purple{background:#bc48ff;box-shadow:0 2px #8f00e1}button.btn-purple:hover,.btn.btn-purple:hover{box-shadow:0 2px 0 1px #8f00e1 inset}button.btn-purple:focus,.btn.btn-purple:focus{background:#8f00e1}button.btn-wire-purple,.btn.btn-wire-purple{background:transparent;box-shadow:none;border:2px solid rgb(188,72,255);color:#000c}button.btn-wire-purple i.btn-match,.btn.btn-wire-purple i.btn-match{color:#bc48ff}button.btn-wire-purple:hover,.btn.btn-wire-purple:hover{background:#bc48ff0d;border-color:#a915ff;color:#a915ff;box-shadow:none}button.btn-wire-purple:focus,.btn.btn-wire-purple:focus{border-color:#8f00e1;background:transparent}button.btn-purple-alt,.btn.btn-purple-alt{background:#692fbd;box-shadow:0 2px #3c1b6b}button.btn-purple-alt:hover,.btn.btn-purple-alt:hover{box-shadow:0 2px 0 1px #3c1b6b inset}button.btn-purple-alt:focus,.btn.btn-purple-alt:focus{background:#3c1b6b}button.btn-wire-purple-alt,.btn.btn-wire-purple-alt{background:transparent;box-shadow:none;border:2px solid rgb(105,47,189);color:#000c}button.btn-wire-purple-alt i.btn-match,.btn.btn-wire-purple-alt i.btn-match{color:#692fbd}button.btn-wire-purple-alt:hover,.btn.btn-wire-purple-alt:hover{background:#692fbd0d;border-color:#522594;color:#522594;box-shadow:none}button.btn-wire-purple-alt:focus,.btn.btn-wire-purple-alt:focus{border-color:#3c1b6b;background:transparent}button.btn-neutral,.btn.btn-neutral{background:#464646;box-shadow:0 2px #131313}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #131313 inset}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#131313}button.btn-wire-neutral,.btn.btn-wire-neutral{background:transparent;box-shadow:none;border:2px solid rgb(70,70,70);color:#000c}button.btn-wire-neutral i.btn-match,.btn.btn-wire-neutral i.btn-match{color:#464646}button.btn-wire-neutral:hover,.btn.btn-wire-neutral:hover{background:#4646460d;border-color:#2d2d2d;color:#2d2d2d;box-shadow:none}button.btn-wire-neutral:focus,.btn.btn-wire-neutral:focus{border-color:#131313;background:transparent}button.btn-yellow,.btn.btn-yellow{background:#ffa70a;box-shadow:0 2px #a36800}button.btn-yellow:hover,.btn.btn-yellow:hover{box-shadow:0 2px 0 1px #a36800 inset}button.btn-yellow:focus,.btn.btn-yellow:focus{background:#a36800}button.btn-wire-yellow,.btn.btn-wire-yellow{background:transparent;box-shadow:none;border:2px solid rgb(255,167,10);color:#000c}button.btn-wire-yellow i.btn-match,.btn.btn-wire-yellow i.btn-match{color:#ffa70a}button.btn-wire-yellow:hover,.btn.btn-wire-yellow:hover{background:#ffa70a0d;border-color:#d68900;color:#d68900;box-shadow:none}button.btn-wire-yellow:focus,.btn.btn-wire-yellow:focus{border-color:#a36800;background:transparent}button.btn-white,.btn.btn-white{background:#fff;box-shadow:0 2px #ccc}button.btn-white:hover,.btn.btn-white:hover{box-shadow:0 2px 0 1px #ccc inset}button.btn-white:focus,.btn.btn-white:focus{background:#ccc}button.btn-wire-white,.btn.btn-wire-white{background:transparent;box-shadow:none;border:2px solid rgb(255,255,255);color:#000c}button.btn-wire-white i.btn-match,.btn.btn-wire-white i.btn-match{color:#fff}button.btn-wire-white:hover,.btn.btn-wire-white:hover{background:#ffffff0d;border-color:#e6e6e6;color:#e6e6e6;box-shadow:none}button.btn-wire-white:focus,.btn.btn-wire-white:focus{border-color:#ccc;background:transparent}button.btn-black,.btn.btn-black{background:#000;box-shadow:0 2px #000}button.btn-black:hover,.btn.btn-black:hover{box-shadow:0 2px 0 1px #000 inset}button.btn-black:focus,.btn.btn-black:focus{background:#000}button.btn-wire-black,.btn.btn-wire-black{background:transparent;box-shadow:none;border:2px solid rgb(0,0,0);color:#000c}button.btn-wire-black i.btn-match,.btn.btn-wire-black i.btn-match{color:#000}button.btn-wire-black:hover,.btn.btn-wire-black:hover{background:#0000000d;border-color:#000;color:#000;box-shadow:none}button.btn-wire-black:focus,.btn.btn-wire-black:focus{border-color:#000;background:transparent}button.btn-youtube,.btn.btn-youtube{background:#fd3c01;box-shadow:0 2px #972401}button.btn-youtube:hover,.btn.btn-youtube:hover{box-shadow:0 2px 0 1px #972401 inset}button.btn-youtube:focus,.btn.btn-youtube:focus{background:#972401}button.btn-wire-youtube,.btn.btn-wire-youtube{background:transparent;box-shadow:none;border:2px solid rgb(253,60,1);color:#000c}button.btn-wire-youtube i.btn-match,.btn.btn-wire-youtube i.btn-match{color:#fd3c01}button.btn-wire-youtube:hover,.btn.btn-wire-youtube:hover{background:#fd3c010d;border-color:#ca3001;color:#ca3001;box-shadow:none}button.btn-wire-youtube:focus,.btn.btn-wire-youtube:focus{border-color:#972401;background:transparent}button.btn-raspbian,.btn.btn-raspbian{background:#b61240;box-shadow:0 2px #59091f}button.btn-raspbian:hover,.btn.btn-raspbian:hover{box-shadow:0 2px 0 1px #59091f inset}button.btn-raspbian:focus,.btn.btn-raspbian:focus{background:#59091f}button.btn-wire-raspbian,.btn.btn-wire-raspbian{background:transparent;box-shadow:none;border:2px solid rgb(182,18,64);color:#000c}button.btn-wire-raspbian i.btn-match,.btn.btn-wire-raspbian i.btn-match{color:#b61240}button.btn-wire-raspbian:hover,.btn.btn-wire-raspbian:hover{background:#b612400d;border-color:#880d30;color:#880d30;box-shadow:none}button.btn-wire-raspbian:focus,.btn.btn-wire-raspbian:focus{border-color:#59091f;background:transparent}button.btn-windows,.btn.btn-windows{background:#12a7e3;box-shadow:0 2px #0b6184}button.btn-windows:hover,.btn.btn-windows:hover{box-shadow:0 2px 0 1px #0b6184 inset}button.btn-windows:focus,.btn.btn-windows:focus{background:#0b6184}button.btn-wire-windows,.btn.btn-wire-windows{background:transparent;box-shadow:none;border:2px solid rgb(18,167,227);color:#000c}button.btn-wire-windows i.btn-match,.btn.btn-wire-windows i.btn-match{color:#12a7e3}button.btn-wire-windows:hover,.btn.btn-wire-windows:hover{background:#12a7e30d;border-color:#0e84b4;color:#0e84b4;box-shadow:none}button.btn-wire-windows:focus,.btn.btn-wire-windows:focus{border-color:#0b6184;background:transparent}button.btn-macos,.btn.btn-macos{background:#b3bcc2;box-shadow:0 2px #7a8a95}button.btn-macos:hover,.btn.btn-macos:hover{box-shadow:0 2px 0 1px #7a8a95 inset}button.btn-macos:focus,.btn.btn-macos:focus{background:#7a8a95}button.btn-wire-macos,.btn.btn-wire-macos{background:transparent;box-shadow:none;border:2px solid rgb(179,188,194);color:#000c}button.btn-wire-macos i.btn-match,.btn.btn-wire-macos i.btn-match{color:#b3bcc2}button.btn-wire-macos:hover,.btn.btn-wire-macos:hover{background:#b3bcc20d;border-color:#97a3ab;color:#97a3ab;box-shadow:none}button.btn-wire-macos:focus,.btn.btn-wire-macos:focus{border-color:#7a8a95;background:transparent}button.btn-debian,.btn.btn-debian{background:#cf084e;box-shadow:0 2px #6d0429}button.btn-debian:hover,.btn.btn-debian:hover{box-shadow:0 2px 0 1px #6d0429 inset}button.btn-debian:focus,.btn.btn-debian:focus{background:#6d0429}button.btn-wire-debian,.btn.btn-wire-debian{background:transparent;box-shadow:none;border:2px solid rgb(207,8,78);color:#000c}button.btn-wire-debian i.btn-match,.btn.btn-wire-debian i.btn-match{color:#cf084e}button.btn-wire-debian:hover,.btn.btn-wire-debian:hover{background:#cf084e0d;border-color:#9e063b;color:#9e063b;box-shadow:none}button.btn-wire-debian:focus,.btn.btn-wire-debian:focus{border-color:#6d0429;background:transparent}button.btn-fedora,.btn.btn-fedora{background:#52a2da;box-shadow:0 2px #236ea3}button.btn-fedora:hover,.btn.btn-fedora:hover{box-shadow:0 2px 0 1px #236ea3 inset}button.btn-fedora:focus,.btn.btn-fedora:focus{background:#236ea3}button.btn-wire-fedora,.btn.btn-wire-fedora{background:transparent;box-shadow:none;border:2px solid rgb(82,162,218);color:#000c}button.btn-wire-fedora i.btn-match,.btn.btn-wire-fedora i.btn-match{color:#52a2da}button.btn-wire-fedora:hover,.btn.btn-wire-fedora:hover{background:#52a2da0d;border-color:#2c8bcd;color:#2c8bcd;box-shadow:none}button.btn-wire-fedora:focus,.btn.btn-wire-fedora:focus{border-color:#236ea3;background:transparent}button.btn-ubuntu,.btn.btn-ubuntu{background:#d64514;box-shadow:0 2px #79270b}button.btn-ubuntu:hover,.btn.btn-ubuntu:hover{box-shadow:0 2px 0 1px #79270b inset}button.btn-ubuntu:focus,.btn.btn-ubuntu:focus{background:#79270b}button.btn-wire-ubuntu,.btn.btn-wire-ubuntu{background:transparent;box-shadow:none;border:2px solid rgb(214,69,20);color:#000c}button.btn-wire-ubuntu i.btn-match,.btn.btn-wire-ubuntu i.btn-match{color:#d64514}button.btn-wire-ubuntu:hover,.btn.btn-wire-ubuntu:hover{background:#d645140d;border-color:#a73610;color:#a73610;box-shadow:none}button.btn-wire-ubuntu:focus,.btn.btn-wire-ubuntu:focus{border-color:#79270b;background:transparent}button.btn-suse,.btn.btn-suse{background:#6fb425;box-shadow:0 2px #3b5f14}button.btn-suse:hover,.btn.btn-suse:hover{box-shadow:0 2px 0 1px #3b5f14 inset}button.btn-suse:focus,.btn.btn-suse:focus{background:#3b5f14}button.btn-wire-suse,.btn.btn-wire-suse{background:transparent;box-shadow:none;border:2px solid rgb(111,180,37);color:#000c}button.btn-wire-suse i.btn-match,.btn.btn-wire-suse i.btn-match{color:#6fb425}button.btn-wire-suse:hover,.btn.btn-wire-suse:hover{background:#6fb4250d;border-color:#558a1c;color:#558a1c;box-shadow:none}button.btn-wire-suse:focus,.btn.btn-wire-suse:focus{border-color:#3b5f14;background:transparent}button.btn-redhat,.btn.btn-redhat{background:#c60200;box-shadow:0 2px #600100}button.btn-redhat:hover,.btn.btn-redhat:hover{box-shadow:0 2px 0 1px #600100 inset}button.btn-redhat:focus,.btn.btn-redhat:focus{background:#600100}button.btn-wire-redhat,.btn.btn-wire-redhat{background:transparent;box-shadow:none;border:2px solid rgb(198,2,0);color:#000c}button.btn-wire-redhat i.btn-match,.btn.btn-wire-redhat i.btn-match{color:#c60200}button.btn-wire-redhat:hover,.btn.btn-wire-redhat:hover{background:#c602000d;border-color:#930100;color:#930100;box-shadow:none}button.btn-wire-redhat:focus,.btn.btn-wire-redhat:focus{border-color:#600100;background:transparent}button.btn-centos,.btn.btn-centos{background:#9b4c88;box-shadow:0 2px #572a4c}button.btn-centos:hover,.btn.btn-centos:hover{box-shadow:0 2px 0 1px #572a4c inset}button.btn-centos:focus,.btn.btn-centos:focus{background:#572a4c}button.btn-wire-centos,.btn.btn-wire-centos{background:transparent;box-shadow:none;border:2px solid rgb(155,76,136);color:#000c}button.btn-wire-centos i.btn-match,.btn.btn-wire-centos i.btn-match{color:#9b4c88}button.btn-wire-centos:hover,.btn.btn-wire-centos:hover{background:#9b4c880d;border-color:#793b6a;color:#793b6a;box-shadow:none}button.btn-wire-centos:focus,.btn.btn-wire-centos:focus{border-color:#572a4c;background:transparent}button.btn-other,.btn.btn-other{background:#ffa70a;box-shadow:0 2px #a36800}button.btn-other:hover,.btn.btn-other:hover{box-shadow:0 2px 0 1px #a36800 inset}button.btn-other:focus,.btn.btn-other:focus{background:#a36800}button.btn-wire-other,.btn.btn-wire-other{background:transparent;box-shadow:none;border:2px solid rgb(255,167,10);color:#000c}button.btn-wire-other i.btn-match,.btn.btn-wire-other i.btn-match{color:#ffa70a}button.btn-wire-other:hover,.btn.btn-wire-other:hover{background:#ffa70a0d;border-color:#d68900;color:#d68900;box-shadow:none}button.btn-wire-other:focus,.btn.btn-wire-other:focus{border-color:#a36800;background:transparent}.alert.alert-warning{color:#e56723;background:#e5672333}.alert.alert-info{color:#027bff;background:#027bff33}.alert.alert-info-alt{color:#075cb7;background:#075cb733}.alert.alert-success,.alert.alert-success-alt{color:#11a948;background:#11a94833}.alert.alert-error{color:#ef0e5d;background:#ef0e5d33}.alert.alert-error-alt{color:#c20941;background:#c2094133}.alert.alert-danger{color:#ef0e5d;background:#ef0e5d33}.alert.alert-danger-alt{color:#c20941;background:#c2094133}.alert.alert-purple{color:#bc48ff;background:#bc48ff33}.alert.alert-purple-alt{color:#692fbd;background:#692fbd33}.alert.alert-neutral{color:#464646;background:#46464633}.alert.alert-yellow{color:#ffa70a;background:#ffa70a33}.alert.alert-white{color:#fff;background:#fff3}.alert.alert-black{color:#000;background:#0003}.alert.alert-youtube{color:#fd3c01;background:#fd3c0133}.alert.alert-raspbian{color:#b61240;background:#b6124033}.alert.alert-windows{color:#12a7e3;background:#12a7e333}.alert.alert-macos{color:#b3bcc2;background:#b3bcc233}.alert.alert-debian{color:#cf084e;background:#cf084e33}.alert.alert-fedora{color:#52a2da;background:#52a2da33}.alert.alert-ubuntu{color:#d64514;background:#d6451433}.alert.alert-suse{color:#6fb425;background:#6fb42533}.alert.alert-redhat{color:#c60200;background:#c6020033}.alert.alert-centos{color:#9b4c88;background:#9b4c8833}.alert.alert-other{color:#ffa70a;background:#ffa70a33}@font-face{font-family:Sixtyfour;src:url(../../webfonts/Sixtyfour-Regular.ttf) format("truetype")}*{font-family:Roboto,Arial,"sans-serif";margin:0;padding:0;box-sizing:border-box}html{background-color:#fff}body,html{height:100%;font-family:Arial,sans-serif}.container{display:flex;height:100vh}.horizontal{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}main{flex:1;display:flex;flex-direction:column}main .main-container{display:flex;flex-direction:column;flex:1;overflow:hidden;align-self:stretch}main .main-container .top-content{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:10px 10px 10px 15px;background:transparent;border-bottom:1px solid #DDD}main .main-container .top-content h1{color:#000;font-weight:600;font-size:24px}main .main-container .top-content .top-actions{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}main .main-container .top-content .top-actions.align-right{justify-content:flex-end;margin-right:10px}main .main-container .top-content .top-actions .btn:first-child{margin-left:0!important}main .main-container .top-content .top-actions .btn,main .main-container .top-content .top-actions button{margin-left:10px}main .main-container .bottom-content{display:flex;flex-direction:row;align-self:stretch;justify-content:flex-start;align-items:flex-start;flex:1;overflow-y:auto;background:radial-gradient(circle at 0% 53%,rgba(2,123,255,.8) 10%,transparent 45%),radial-gradient(circle at 135% 53%,rgba(2,123,255,.8) 10%,transparent 95%),radial-gradient(circle at 50% 80%,rgba(105,47,189,.8) 40%,transparent 95%)}main .main-container .bottom-content .page-content{flex:2;overflow-y:auto;align-self:stretch;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;background:#fffc;padding:5px}main .main-container .bottom-content .page-content .inner{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;padding:10px 10px 40px;background:#fff;align-self:stretch}main .main-container .bottom-content .page-content .inner h1,main .main-container .bottom-content .page-content .inner h2,main .main-container .bottom-content .page-content .inner h3,main .main-container .bottom-content .page-content .inner h4,main .main-container .bottom-content .page-content .inner h5,main .main-container .bottom-content .page-content .inner h6{color:#222}main .main-container .bottom-content .page-content .inner p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;color:#999}main .main-container .bottom-content .page-panel{flex:1;overflow-y:auto;align-self:stretch;background:#fff;border-top:none}main .main-container .bottom-content .page-panel.left-panel{border-right:1px solid #DDD;border-left:none}main .main-container .bottom-content .page-panel.left-panel.explr-explorer{flex:.5;overflow-y:auto;padding:0;background:#fff;box-shadow:1px 1px .5px .5px inset #fff3;max-width:250px}main .main-container .bottom-content .page-panel.right-panel{border-left:1px solid #DDD;border-right:none}.invisible{visibility:hidden!important}.hidden{display:none!important}.tac{text-align:center!important}.tar{text-align:right!important}a{text-decoration:none}.normal{font-weight:400!important}.bold{font-weight:700!important}.col{display:flex;flex:1;flex-direction:column;align-self:stretch}main .context-bar{padding:10px;position:sticky;top:0;z-index:1000;max-height:80px;border-bottom:1px solid #DDD;display:flex;flex-direction:row;align-items:center}main .context-bar .context-menu{flex:1}main .context-bar .context-menu .inner{display:flex}main .context-bar .context-menu .inner ul.pills{margin:0}main .context-bar .context-divider{width:1px;height:100%;background:#ddd;margin-left:20px;margin-right:20px}main .context-bar .contex-tail{margin-right:20px}main .context-bar .contex-tail .btn{margin-right:0}main .context-bar .context-user{display:flex;margin-right:20px}main .context-bar .context-user .trigger{color:#000}main .context-bar .context-user .trigger .avatar{width:32px;height:32px;border-radius:4px;background:#555;margin-right:10px;display:flex;flex-direction:row;justify-content:center;align-items:center;text-align:center;font-weight:700;font-size:14px;border:1px solid #BBB;color:#fff}main .context-bar .context-user .trigger i{margin-top:-5px;margin-left:10px}menu{width:300px;background:#fff;overflow-y:auto;overflow-x:visible;padding:20px;z-index:2000;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;border-right:1px solid #DDD;min-width:64px}menu h1.logo{margin:40px 0 0 10px;align-self:stretch;display:flex}menu h1.logo a{text-align:center;text-shadow:0px 0 0 rgb(255,255,255),0px 2px 0 #BBB,0 0px 0 rgb(17,169,72),-0px 0 0 rgb(2,123,255),0 -0px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent;flex:1;font-family:Sixtyfour,Work Sans,Arial,"sans-serif";align-self:stretch;padding-right:3px;font-size:20px;text-transform:uppercase;transition:all .55s cubic-bezier(.19,1,.22,1);display:flex;flex-direction:row;justify-content:center;align-items:center;position:relative;color:#fff}menu h1.logo a img{flex-shrink:0;width:30px;margin-right:10px;position:absolute;left:5px;transition:all .55s cubic-bezier(.19,1,.22,1)}menu h1.logo a img.after{opacity:0}menu:hover h1.logo a{text-align:center;text-shadow:3px 0 0 rgb(255,255,255),3px 2px 0 #BBB,0 3px 0 rgb(17,169,72),-3px 0 0 rgb(2,123,255),0 -3px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}menu:hover h1.logo a img.before{opacity:0}menu:hover h1.logo a img.after{animation-duration:.2s;animation-name:logotouch}menu nav{display:flex;align-self:stretch;flex:1}menu nav ul{margin:60px 0 20px;flex:1;align-self:flex-start;display:flex;flex-direction:column;list-style:none}menu nav ul li{align-self:stretch;overflow:hidden;position:relative;transition:all .55s cubic-bezier(.19,1,.22,1);margin:10px 0;border-radius:4px}menu nav ul li a{color:#000000e6;font-size:16px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1;padding-top:5px;padding-bottom:5px;padding-left:10px}menu nav ul li a i{color:#000;opacity:.2;background:transparent;display:flex;justify-content:center;align-items:center;align-self:stretch;padding:10px;width:40px;border-radius:4px;text-align:center;margin-right:20px}menu nav ul li:after{background:#000;content:"";height:195px;left:-200px;opacity:.2;position:absolute;top:-50px;transform:rotate(35deg);transition:all .55s cubic-bezier(.19,1,.22,1);width:50px;z-index:-2;cursor:pointer}menu nav ul li.active a{color:#027bff;font-weight:700}menu nav ul li.active a i{opacity:1;color:#fff;background:#000000e6;background:#027bff}menu nav ul li:hover{background:#027bff}menu nav ul li:hover:after{z-index:2;left:120%;transition:all .55s cubic-bezier(.19,1,.22,1)}menu nav ul li:hover a{color:#fff;font-weight:700}menu nav ul li:hover a i{color:#fff;opacity:1}menu footer{background:#00000003;padding:20px 0;display:flex;flex-direction:row;align-self:stretch;text-align:center;justify-content:center}menu footer p{color:#bbb}menu footer p.version a{color:#888;font-weight:700}.dropdown{position:relative;display:flex;align-self:stretch}.dropdown.dropdown-show ul.dropdown-menu{display:flex;flex-direction:column}.dropdown .trigger{cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1}.dropdown ul.dropdown-menu{position:absolute;top:100%;left:0;display:none;background-color:#ddd;box-shadow:0 8px 16px #fff3;z-index:1000;list-style-type:none;margin:0;overflow:hidden;border-radius:4px}.dropdown ul.dropdown-menu li{padding:8px 16px;cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;transition:all .55s cubic-bezier(.19,1,.22,1)}.dropdown ul.dropdown-menu li.danger:hover{background-color:#ef0e5d}.dropdown ul.dropdown-menu li:hover{background-color:#027bff}.dropdown ul.dropdown-menu li a{padding:8px 16px 8px 8px;color:#000;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch}.dropdown ul.dropdown-menu li a i{margin-right:15px}@keyframes logotouch{0%{opacity:0}50%{opacity:1}to{opacity:0;left:27px}}@keyframes shakednd{0%{transform:rotate(-2deg)}to{transform:rotate(2deg)}}button,.btn{position:relative;padding:10px 10px 8px;font-size:14px;color:#fff;cursor:pointer;border:none;border-radius:4px;background:#027bff;box-shadow:0 2px #004a9b;font-weight:700;letter-spacing:-.5px;margin-top:-2px;min-width:38px;min-height:34px;text-align:center;justify-content:center}button i.icon-left,.btn i.icon-left{margin-right:5px}button:hover,.btn:hover{box-shadow:0 2px 0 1px #004a9b inset;color:#fffc}button:focus,.btn:focus{background:#004a9b;color:#ffffff80;box-shadow:none}button.btn-pixel,.btn.btn-pixel{background:#333;border:1px solid transparent;transition:all .55s cubic-bezier(.19,1,.22,1);text-transform:uppercase;font-size:12px;box-shadow:4px 0 #fff,0 4px #11a948,-4px 0 #027bff,0 -4px #ef0e5d;color:#ddd;overflow:hidden}button.btn-pixel:hover,.btn.btn-pixel:hover{box-shadow:6px 0 #fff,0 6px #11a948,-6px 0 #027bff,0 -6px #ef0e5d;text-align:center;text-shadow:4px 0 0 rgb(255,255,255),4px 2px 0 #BBB,0 4px 0 rgb(17,169,72),-4px 0 0 rgb(2,123,255),0 -4px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}button.btn-neutral,.btn.btn-neutral{color:#fff;background:#aaa;box-shadow:0 2px #919191;border:1px solid transparent}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #222 inset;background:#919191}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#777;border:1px solid #555}button .btn-wire-neutral,.btn .btn-wire-neutral{background:transparent;border:2px solid #AAA;color:#fffc;box-shadow:none}button .btn-wire-neutral:hover,.btn .btn-wire-neutral:hover{background:#aaaaaa0d;border-color:#919191;color:#919191;box-shadow:none}button .btn-wire-neutral:focus,.btn .btn-wire-neutral:focus{border-color:#777;background:transparent}button.btn-naked,.btn.btn-naked{background:transparent;box-shadow:none;border:1px solid transparent;color:#000}button.btn-naked:hover,.btn.btn-naked:hover{box-shadow:0 2px 0 1px #222 inset;background:#919191;color:#fff}button.btn-naked:focus,.btn.btn-naked:focus{background:#777;border:1px solid #555}button i.main,.btn i.main{font-size:18px}button sup,button sub,.btn sup,.btn sub{position:absolute;top:-4px;right:-4px;background:#888;border-bottom:2px solid #AAA;color:red;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}button sup i,button sub i,.btn sup i,.btn sub i{color:#fff;font-size:10px}button.btn-double-icon,.btn.btn-double-icon{margin-right:5px}button.disabled,.btn.disabled{cursor:default}.alert{padding:20px;align-self:stretch;display:flex;flex-direction:row;justify-content:center;align-items:center;border-radius:4px}.alert i{margin-right:13px}.alert a{color:inherit;margin-left:4px;margin-right:4px;text-decoration:underline}ul.explr-tree{height:100%!important}ul.explr-tree ul{padding-top:0!important}ul.explr-tree li{position:relative}ul.explr-tree li span{color:#aaa;font-size:13px;padding-left:5px;cursor:pointer}ul.explr-tree li span.explr-plus,ul.explr-tree li span.explr-minus{z-index:1}ul.explr-tree li span.explr-plus:hover,ul.explr-tree li span.explr-minus:hover{color:#555}ul.explr-tree li i.main{font-size:14px}ul.explr-tree li sup,ul.explr-tree li sub{position:absolute;top:0;left:5px;background:#888;border-bottom:2px solid #AAA;border-radius:4px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-tree li sup i,ul.explr-tree li sub i{color:#fff;font-size:4px!important;margin-bottom:0}ul.explr-tree li a{color:#000;padding-right:80px;margin-top:2px}ul.explr-tree li a:hover{color:#000}ul.explr-tree li a.active{background:#0000001a;border-radius:4px;font-weight:700;text-decoration:underline;margin-left:35px;padding-left:5px;margin-right:10px}.explr-multiselection-actions,.explr-selection-actions{display:none;flex-direction:row;justify-content:flex-end;align-items:center;flex:1}.explr-multiselection-actions button,.explr-selection-actions button{display:none}body.explr-selection-actionable .explr-selection-actions,body.explr-selection-actionable.explr-selection-folder .explr-selection-actions button.explr-selection-folder,body.explr-selection-actionable.explr-selection-entity .explr-selection-actions button.explr-selection-entity,body.explr-multiselection-actionable .explr-multiselection-actions,body.explr-multiselection-actionable.explr-multiselection-folder .explr-multiselection-actions button.explr-multiselection-folder,body.explr-multiselection-actionable.explr-multiselection-entity .explr-multiselection-actions button.explr-multiselection-entity{display:flex}.selectable-zone{flex:1;align-self:stretch;border:1px solid transparent}ul.explr-dirview{display:flex;flex-direction:row;flex-wrap:wrap}ul.explr-dirview li{display:flex;flex-direction:column;justify-content:flex-start;align-items:center;flex-shrink:0;margin:10px;min-width:100px;min-height:130px;padding-top:5px;border:1px solid transparent;border-radius:4px}ul.explr-dirview li.renaming a span{display:none}ul.explr-dirview li.renaming a form{display:block}ul.explr-dirview li.highlight-drop{border:1px dotted rgba(2,123,255,.4);background:#027bff4d}ul.explr-dirview li.highlight-clicked{border:1px dotted rgba(0,0,0,.2);background:#0000001a}ul.explr-dirview li a{color:#444;text-decoration:none;flex:1;text-align:center;font-size:12px;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;max-width:84px;min-width:84px;position:relative;word-break:break-all}ul.explr-dirview li a.with-thumbnail .img-holder{width:64px;height:64px;background:#000;border-radius:8px;display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden;margin-bottom:12px}ul.explr-dirview li a.with-thumbnail .img-holder img{max-height:100%;max-width:100%}ul.explr-dirview li a.with-thumbnail i{font-size:24px;position:absolute;top:-4px;left:-4px;text-shadow:0 .5px .5px #888}ul.explr-dirview li a i{font-size:64px;margin-bottom:12px;border-radius:8px}ul.explr-dirview li a sup,ul.explr-dirview li a sub{position:absolute;top:-2px;right:0;background:#888;border-bottom:2px solid #AAA;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-dirview li a sup i,ul.explr-dirview li a sub i{color:#fff;font-size:10px;margin-bottom:0}ul.explr-dirview li a input{width:100%;padding:0 3px}ul.explr-dirview li a input:focus{outline:none}ul.explr-dirview li a:hover{opacity:.8}ul.explr-dirview li a form{display:none}ul.explr-dirview li.new-folder a{color:#027bff}ul.explr-dirview li.new-folder a form{display:block}ul.explr-dirview .ui-draggable-dragging{z-index:20}ul.explr-dirview .ui-draggable-dragging a{opacity:1!important}.modal-explr-picker h2{margin-top:0}.modal-explr-picker .explr-tree{width:400px;max-width:400px;max-height:300px;overflow:auto;background:#ddd;padding:10px 20px;border-radius:4px;margin-top:15px}.content-explr-picker{cursor:pointer}.selection-rectangle{position:absolute;border:1px solid rgb(0,153,255);background-color:#0099ff1a;pointer-events:none;z-index:1000}ul.pills{background:#ddd;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #fff3;border:1px solid #DDD;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0}ul.pills li.divider{margin:0 20px;width:1px;height:100%;background:#ccc}ul.pills li:hover a{opacity:.9}ul.pills li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#000;overflow:hidden;padding-right:30px;text-align:center;background:#fff3;margin-right:5px;transition:all .25s cubic-bezier(.19,1,.22,1)}ul.pills li a span{display:flex;justify-content:center;align-items:center;margin-right:20px;height:42px;background:#fff3;width:42px}ul.pills li.active a{color:#ccc;background:#333;font-weight:700}ul.pills li:hover a{color:#fff;background:#027bff}ul.pills li:last-child a{margin-right:0}.breadcrumb-container{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;border-bottom:1px solid #DDD;background:transparent;padding:10px}.breadcrumb-container ul.breadcrumb{background:#ddd;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #fff3;border:1px solid #DDD;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0;overflow-x:auto;max-width:65vw;white-space:nowrap}.breadcrumb-container ul.breadcrumb li{display:inline-block}.breadcrumb-container ul.breadcrumb li.divider{margin:0 5px}.breadcrumb-container ul.breadcrumb li.divider i{color:#555}.breadcrumb-container ul.breadcrumb li span,.breadcrumb-container ul.breadcrumb li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#000;text-align:center;padding:0 3px}.breadcrumb-container ul.breadcrumb li span i,.breadcrumb-container ul.breadcrumb li a i{margin-right:5px}.breadcrumb-container ul.breadcrumb li:hover a{color:#000;background:#027bff}.breadcrumb-container ul.breadcrumb li:last-child a{margin-right:0}.pickers,.modals{position:fixed;background:#0006;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:10000}.pickers.pickers .modals-outer .modals-inner .modal h2,.modals.pickers .modals-outer .modals-inner .modal h2{font-size:14px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.pickers .modals-outer,.modals .modals-outer{min-width:464px;max-width:464px;display:flex;flex-direction:column;overflow:auto;padding-bottom:2px}.pickers .modals-outer .modals-inner,.modals .modals-outer .modals-inner{background:#fff;border-radius:10px;color:#fff;padding:40px;box-shadow:0 2px #ddd;border:1px solid #DDD}.pickers .modals-outer .modals-inner .modal h2,.modals .modals-outer .modals-inner .modal h2{padding:0;margin:0 0 30px;font-weight:400;color:#666}.pickers .modals-outer .modals-inner .modal h3,.modals .modals-outer .modals-inner .modal h3{align-self:stretch;margin:0 0 10px;font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#000;padding-bottom:10px;text-decoration:none}.pickers .modals-outer .modals-inner .modal h3.divide,.modals .modals-outer .modals-inner .modal h3.divide{border-top:1px solid #DDD;margin-top:20px;padding-top:20px}.toast{visibility:hidden;min-width:250px;margin-left:-125px;background-color:#ccc;color:#000;text-align:center;border-radius:4px;padding:16px;position:fixed;z-index:1;left:50%;bottom:30px;font-size:17px;border:2px dashed #DDD}.toast.show{visibility:visible;-webkit-animation:fadein .5s,fadeout .5s 2.5s;animation:fadein .5s,fadeout .5s 2.5s}@-webkit-keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@-webkit-keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}@keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}body.dragover .shakeondrag{animation:shakednd .1s linear alternate infinite}.btn-super-upload-busy,.btn-super-upload{display:flex;flex-direction:row;justify-content:center;align-items:center;margin-left:10px;position:relative}.btn-super-upload-busy.btn-super-upload-busy,.btn-super-upload.btn-super-upload-busy{border:none!important}.btn-super-upload-busy .unprogress,.btn-super-upload .unprogress{display:block}.btn-super-upload-busy .progress,.btn-super-upload .progress{display:none;width:200px;height:10px;background:#999;border-radius:4px;flex-direction:row;justify-content:flex-start;align-items:center}.btn-super-upload-busy .progress .progress-bar,.btn-super-upload .progress .progress-bar{border-radius:4px;background-color:#027bff;height:100%}.btn-super-upload-busy .progress .percent,.btn-super-upload .progress .percent{display:flex;justify-content:center;align-items:center;position:absolute;left:0;right:0;bottom:0;top:2px;font-size:15px;color:#fff;text-shadow:0 0 2px rgb(0,0,0)}.btn-super-upload-busy.uploading .progress,.btn-super-upload.uploading .progress{display:block}.btn-super-upload-busy.uploading .unprogress,.btn-super-upload.uploading .unprogress{display:none}.panes{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.panes .pane-section,.panes tbody{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:1px 1px 28px;background:#fff;border-radius:4px;border:4px solid rgba(0,0,0,.05)}.panes .pane-section:hover,.panes tbody:hover,.panes .pane-section:hover tr.title-item,.panes tbody:hover tr.title-item{border-color:#027bff0d}.panes .pane-section .pane-item,.panes .pane-section tr,.panes tbody .pane-item,.panes tbody tr{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:8px 18px;background:#ddd}.panes .pane-section .pane-item:nth-child(odd),.panes .pane-section tr:nth-child(odd),.panes tbody .pane-item:nth-child(odd),.panes tbody tr:nth-child(odd){background-color:#eee}.panes .pane-section .pane-item:nth-child(odd) td.description,.panes .pane-section tr:nth-child(odd) td.description,.panes tbody .pane-item:nth-child(odd) td.description,.panes tbody tr:nth-child(odd) td.description{color:#000000b3}.panes .pane-section .pane-item:nth-child(2n),.panes .pane-section tr:nth-child(2n),.panes tbody .pane-item:nth-child(2n),.panes tbody tr:nth-child(2n){background-color:#e4e4e4}.panes .pane-section .pane-item .pane-cell,.panes .pane-section .pane-item td,.panes .pane-section tr .pane-cell,.panes .pane-section tr td,.panes tbody .pane-item .pane-cell,.panes tbody .pane-item td,.panes tbody tr .pane-cell,.panes tbody tr td{color:#777;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;flex:1;font-size:14px}.panes .pane-section .pane-item .pane-cell.vertical,.panes .pane-section .pane-item td.vertical,.panes .pane-section tr .pane-cell.vertical,.panes .pane-section tr td.vertical,.panes tbody .pane-item .pane-cell.vertical,.panes tbody .pane-item td.vertical,.panes tbody tr .pane-cell.vertical,.panes tbody tr td.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}.panes .pane-section .pane-item .pane-cell.description,.panes .pane-section .pane-item td.description,.panes .pane-section tr .pane-cell.description,.panes .pane-section tr td.description,.panes tbody .pane-item .pane-cell.description,.panes tbody .pane-item td.description,.panes tbody tr .pane-cell.description,.panes tbody tr td.description{align-self:stretch}.panes .pane-section .pane-item .pane-cell.value,.panes .pane-section .pane-item td.value,.panes .pane-section tr .pane-cell.value,.panes .pane-section tr td.value,.panes tbody .pane-item .pane-cell.value,.panes tbody .pane-item td.value,.panes tbody tr .pane-cell.value,.panes tbody tr td.value{flex:0;margin-left:20px;word-break:break-all;flex-basis:auto}.panes .pane-section .pane-item .pane-cell.value i.icon-legend,.panes .pane-section .pane-item td.value i.icon-legend,.panes .pane-section tr .pane-cell.value i.icon-legend,.panes .pane-section tr td.value i.icon-legend,.panes tbody .pane-item .pane-cell.value i.icon-legend,.panes tbody .pane-item td.value i.icon-legend,.panes tbody tr .pane-cell.value i.icon-legend,.panes tbody tr td.value i.icon-legend{font-size:10px;margin-right:10px}.panes .pane-section .pane-item .pane-cell.value i.icon-value,.panes .pane-section .pane-item td.value i.icon-value,.panes .pane-section tr .pane-cell.value i.icon-value,.panes .pane-section tr td.value i.icon-value,.panes tbody .pane-item .pane-cell.value i.icon-value,.panes tbody .pane-item td.value i.icon-value,.panes tbody tr .pane-cell.value i.icon-value,.panes tbody tr td.value i.icon-value{padding:2px 2px 1px;border-radius:2px;width:16px;text-align:center}.panes .pane-section .pane-item.title-item,.panes .pane-section tr.title-item,.panes tbody .pane-item.title-item,.panes tbody tr.title-item{border-bottom:1px solid #DDD}.panes .pane-section .pane-item.title-item td,.panes .pane-section tr.title-item td,.panes tbody .pane-item.title-item td,.panes tbody tr.title-item td{color:#027bffe6;font-size:16px;font-weight:700}.panes .pane-section .pane-item.title-item td i,.panes .pane-section tr.title-item td i,.panes tbody .pane-item.title-item td i,.panes tbody tr.title-item td i{margin-right:10px}.panes .pane-section .pane-item.title-item td .more,.panes .pane-section tr.title-item td .more,.panes tbody .pane-item.title-item td .more,.panes tbody tr.title-item td .more{flex:1;text-align:right;font-size:12px;color:#777;font-weight:400;font-style:italic}.panes .pane-section .pane-item.variable-item,.panes .pane-section tr.variable-item,.panes tbody .pane-item.variable-item,.panes tbody tr.variable-item{cursor:pointer}.panes .pane-section .pane-item.variable-item:hover,.panes .pane-section tr.variable-item:hover,.panes tbody .pane-item.variable-item:hover,.panes tbody tr.variable-item:hover{background-color:#027bff0d}.panes .pane-section .pane-item.variable-item:hover td,.panes .pane-section tr.variable-item:hover td,.panes tbody .pane-item.variable-item:hover td,.panes tbody tr.variable-item:hover td{font-weight:700;color:#000}.panes .pane-section .pane-item.variable-item:hover td i.icon-legend,.panes .pane-section tr.variable-item:hover td i.icon-legend,.panes tbody .pane-item.variable-item:hover td i.icon-legend,.panes tbody tr.variable-item:hover td i.icon-legend{color:#000}.panes .pane-section .pane-item.variable-item:hover td span,.panes .pane-section .pane-item.variable-item:hover td i.icon-value,.panes .pane-section tr.variable-item:hover td span,.panes .pane-section tr.variable-item:hover td i.icon-value,.panes tbody .pane-item.variable-item:hover td span,.panes tbody .pane-item.variable-item:hover td i.icon-value,.panes tbody tr.variable-item:hover td span,.panes tbody tr.variable-item:hover td i.icon-value{background-color:#0000004d;color:#000}.panes .pane-section .pane-item.variable-item:hover td.description,.panes .pane-section tr.variable-item:hover td.description,.panes tbody .pane-item.variable-item:hover td.description,.panes tbody tr.variable-item:hover td.description{color:#027bff}.tiles{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;align-self:stretch}.tiles .tiles-inner{display:flex;flex:1;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;align-self:stretch;padding:2px}.tiles .tiles-inner .tile-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#000;margin:1px;padding:15px 10px 15px 15px;border-radius:4px;border-bottom:1px solid transparent}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-left:4px solid rgb(2,123,255);border-radius:4px;border-bottom:2px solid #E7E7E7;background:#ddd;color:#027bff}.tiles .tiles-inner .tile-item:hover:hover,.tiles .tiles-inner .tile-item.active:hover{opacity:1}.tiles .tiles-inner .tile-item:hover.disabled,.tiles .tiles-inner .tile-item.active.disabled{border-left-color:#bbb;color:#000}.tiles .tiles-inner .tile-item:hover.starred,.tiles .tiles-inner .tile-item.active.starred{border-left-color:#ffa70a;color:#ffa70a}.tiles .tiles-inner .tile-item:hover.starred .tile-tail .head-icon i,.tiles .tiles-inner .tile-item.active.starred .tile-tail .head-icon i{opacity:1;color:#ffa70a;font-size:8px}.tiles .tiles-inner .tile-item.starred .tile-tail .head-icon i{font-size:8px;color:#ffa70a}.tiles .tiles-inner .tile-item.disabled .tile-body{opacity:.3}.tiles .tiles-inner .tile-item.disabled .tile-tail .head-icon i{color:#bbb;opacity:.4}.tiles .tiles-inner .tile-item .tile-tail{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin-left:10px;min-width:10px}.tiles .tiles-inner .tile-item .tile-tail .head-icon{flex:1;display:flex;text-align:right;flex-direction:row;justify-content:center;align-items:center;align-self:stretch}.tiles .tiles-inner .tile-item .tile-tail .head-icon i{font-size:6px;display:flex}.tiles .tiles-inner .tile-item .tile-tail .status-icons{display:flex;flex-direction:row;justify-content:flex-end;align-items:center}.tiles .tiles-inner .tile-item .tile-tail .status-icons i{font-size:16px;margin-left:10px}.tiles .tiles-inner .tile-item .tile-body{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;font-size:15px;font-weight:400;letter-spacing:.8px;line-height:22px;margin:0;flex-wrap:nowrap}.tiles .tiles-inner .tile-item .tile-body i{font-size:8px;margin-right:5px}.tiles .tiles-inner .tile-item .tile-metrics{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;margin:0}.tiles .tiles-inner .tile-item .tile-metrics .foot-span span,.tiles .tiles-inner .tile-item .tile-metrics .foot-span{opacity:.8;font-size:13px;font-family:Courier New}span.empty{background:#ffa70a4d;color:#ffa70a;text-transform:lowercase;border-radius:2px;padding:2px 4px;font-weight:700}.inner-empty{display:flex;flex:1;flex-direction:column;align-self:stretch;justify-content:center;align-items:center}.inner-empty i{font-size:90px;opacity:.3;text-shadow:0 -1px #CCC,0 0px .5px #BBB}.toggle{position:relative;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.toggle input{display:none}.toggle input:checked+label{background:#027bff;border:1px solid rgba(0,0,0,.1);box-shadow:0 2px 2px #222 inset}.toggle input:checked+label:after{content:"";display:block;border-radius:50%;margin-left:21px;width:18px;height:18px;transition:.2s;background:#9bcbff;box-shadow:0 2px #0063ce}.toggle label{width:44px;height:26px;border-radius:15px;background:#ddd;cursor:pointer;border:1px solid rgba(0,0,0,.1);box-shadow:0 2px 2px #111 inset}.toggle label:after{content:"";display:block;border-radius:50%;width:18px;height:18px;margin:3px;background:#888;box-shadow:0 2px #555555e6;transition:.2s}.form-holder{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.form-holder form{max-width:434px}form{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}form .alert{padding:8px 15px;font-size:14px;margin:0 0 25px}.from-group-condition{flex-direction:column;align-self:stretch;justify-content:flex-start;align-items:flex-start;display:flex}.form-group{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;width:100%;flex:1;margin-bottom:20px}.form-group label{flex:1;font-size:12px;line-height:18px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.form-group label.btn-upload{color:#000;font-size:14px;flex:0;flex-basis:auto;margin-top:5px}.form-group label.btn-upload input[type=file]{display:none}.form-group label.btn-upload input[type=text]{margin-bottom:2px;margin-left:10px}.form-group label.btn-upload span.btn{padding-right:20px}.form-group label.btn-upload i{margin-left:3px;margin-right:10px}.form-group .widget{margin-top:10px;align-self:stretch;display:flex;flex-direction:row}.form-group .widget.vertical{flex-direction:column}.form-group .widget.vertical select,.form-group .widget.vertical input{align-self:stretch}.form-group .widget.vertical select:first-child,.form-group .widget.vertical input:first-child{margin-bottom:10px}.form-group .widget .btn{margin-left:10px}.form-group .widget.widget-unit select,.form-group .widget.widget-unit input{flex-grow:0;background:none;box-shadow:none;border:none;border-bottom:1px solid #CCC;border-radius:0;max-width:80px;padding-left:0;color:#000;text-align:center}.form-group .widget.widget-unit span{font-size:12px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-left:5px;color:#aaa}.form-group .widget div{color:#000000b3;font-size:14px}.form-group .widget select,.form-group .widget input,.form-group .widget textarea{outline:none;padding:8px 0 5px 8px;border-radius:2px;border:1px solid rgba(0,0,0,.05);flex:1;background:#aaa;box-shadow:0 2px 1px #bbb,0 4px 2px #ccc inset;color:#222;font-size:14px}.form-group .widget select.input-naked,.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.input-naked,.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.input-naked,.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{color:#aaa;background:none;box-shadow:none;border:none;border-bottom:1px solid #CCC;border-radius:0}.form-group .widget select.input-naked,.form-group .widget input.input-naked,.form-group .widget textarea.input-naked{padding-left:0;color:#444}.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{border:none;background:#fff;border-radius:4px;padding-left:10px;padding-right:10px}.form-group.tab-select{border-bottom:1px solid #BBB;display:flex;flex-direction:row;position:relative;height:48px;padding:48px 0 0;flex:0;flex-basis:auto}.form-group.tab-select .widget{height:49px;margin-top:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;position:absolute;top:0;left:0;border-bottom:2px solid rgb(2,123,255);color:#027bff}.form-group.tab-select .widget select{border:none;background:none;box-shadow:none;padding:10px 35px 10px 10px;margin:0;color:inherit;appearance:none;-moz-appearance:none;-webkit-appearance:none;text-align:left;font-weight:700;cursor:pointer;border-radius:4px 4px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;z-index:2}.form-group.tab-select .widget i{margin-left:10px;margin-right:0}.form-group.tab-select .widget i.triangle{margin-top:-4px;margin-left:0;position:absolute;right:10px}.form-group.form-group-horizontal{margin:10px 0 20px;flex-direction:row;justify-content:flex-start;align-items:center}.form-group.form-group-horizontal .widget{margin:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.actions{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin:20px 0 0}.actions.actions-intermediate{margin:0}.actions button{margin-left:25px}.actions.actions-left{justify-content:flex-start}.actions.actions-left .btn{margin-left:0;margin-right:25px}.actions.actions-right{justify-content:flex-end}.actions.actions-right .btn{margin-left:10px;margin-right:0}.actions.actions-center{justify-content:center}.actions.actions-center .btn{margin-left:0;margin-right:0}.view-content-list main .main-container .page-content .inner{padding-bottom:10px}.view-content-list main .main-container .content-object-input{margin-bottom:6px}.view-content-list.dragover main .main-container .inner .dropzone{border-radius:4px;background:#0000001a;border:1px dashed rgba(0,0,0,.5)}.view-content-edit main .main-container .bottom-content .page-content{flex:1}.view-content-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px;flex:1}.view-content-edit main .main-container .bottom-content .page-panel.right-panel{flex:2;align-self:stretch;display:flex;flex-direction:column;overflow:hidden;justify-content:flex-start;align-items:center;padding:20px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3{color:#000;padding:10px 10px 10px 0;margin-bottom:20px;font-size:16px;align-self:stretch;margin-left:-8px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 span{border-width:1px;border-style:solid;border-radius:4px;padding:4px 10px;margin-left:5px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 i{font-size:16px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper{display:flex;flex-direction:column;width:100%;position:relative;padding-top:56.25%;overflow:hidden;border-radius:4px;outline:4px solid rgba(0,0,0,.1)}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.view-logs-list main .main-container .bottom-content .page-content .inner{padding-top:8px;padding-bottom:8px}.view-logs-list main .main-container .bottom-content .page-content .logs{flex:1;display:flex;flex-direction:column;align-self:stretch}.view-logs-list main .main-container .bottom-content .page-content .logs pre{flex:1;background:#000000e6;border:1px solid rgba(85,85,85,.5);border-radius:4px;font-family:monospace;color:#f2f2f2;padding:20px;overflow:auto;align-self:stretch}.view-node-player-edit main .main-container .bottom-content .page-content{flex:1}.view-node-player-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px}.view-player-group-list main .main-container .players-holder ul.players{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #DDD;border-radius:4px;padding:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .head{display:flex;flex-direction:column;justify-content:center;align-items:center;color:#666;font-size:10px;padding:10px;cursor:default}.view-player-group-list main .main-container .players-holder ul.players li.player-item:hover .infos .title{color:#000}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px;flex:1;max-width:180px;background:#fff;border:1px solid #CCC;border-radius:4px;padding:3px 7px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .title{font-size:13px;color:#555;display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body{display:block;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#ebebeb;padding:10px;align-self:stretch;flex:1;border-radius:4px;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px;font-size:12px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body span{opacity:.5;margin-right:7px;font-size:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a{color:#000}.view-playlist-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.view-playlist-list main .main-container .modal-playlist-qrcode h2{text-align:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic{text-align:center;display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic img{border:4px solid #AAA;border-radius:4px}.view-playlist-list main .main-container .modal-slide h2{font-size:20px}.view-playlist-list main .main-container .modal-slide input[disabled]{color:#555}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select{margin-right:5px}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input{font-size:12px;max-width:50%}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input.datetimepicker{margin-left:5px;padding-left:0}.view-playlist-list main .main-container .bottom-content .page-content{flex:1}.view-playlist-list main .main-container .bottom-content .page-content.with-right-panel{flex:.5}.view-playlist-list main .main-container .bottom-content .page-content .inner{padding:0}.view-playlist-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#222;text-decoration:none;margin:0 0 20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder{margin:20px 20px 20px 10px;flex:1}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder{margin:20px 0 0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder form{max-width:initial}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder .form-group{flex-grow:0;margin-bottom:5px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder{position:relative}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .hover-only{display:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#000;padding-bottom:10px;text-decoration:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4.divide{border-top:1px solid #DDD;margin-top:20px;padding-top:20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic{margin-top:10px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic img{border:1px dashed #AAA;padding:5px;border-radius:4px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview{background:#000;border:1px solid rgba(0,0,0,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview iframe{flex:1;align-self:stretch}.view-playlist-list main .main-container .bottom-content .page-content .inner .slides-holder{align-self:stretch;border-right:1px solid #DDD;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-playlist-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-player-group-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.view-player-group-list main .main-container .bottom-content .page-content{flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner{padding:0}.view-player-group-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#222;text-decoration:none;margin:0 0 20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder{margin:20px 20px 20px 10px;flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder{margin:20px 0 0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder form{max-width:initial}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder .form-group{flex-grow:0;margin-bottom:15px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder{position:relative}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .hover-only{display:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#000;padding-bottom:10px;text-decoration:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4.divide{border-top:1px solid #DDD;margin-top:20px;padding-top:20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview{background:#000;border:1px solid rgba(0,0,0,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview iframe{flex:1;align-self:stretch}.view-player-group-list main .main-container .bottom-content .page-content .inner .players-holder{align-self:stretch;border-right:1px solid #DDD;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-player-group-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-playlist-list main .main-container .page-content .inner h3.divide{margin-top:50px}.view-playlist-list main .main-container .slides-holder ul.slides{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #DDD;border-radius:4px;padding:10px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .title{color:#ccc}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .type i{color:#ccc!important}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .body{opacity:0}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort{display:flex;flex-direction:column;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a{color:#666;font-size:10px;padding:10px;cursor:move}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a:hover{color:#027bff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px;flex:1;max-width:120px;background:#fff;border:1px solid #CCC;border-radius:4px;padding:3px 7px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos:hover .title{color:#000}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .title{display:block;word-break:break-all;font-size:13px;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#ebebeb;padding:10px;align-self:stretch;flex:1;border-radius:4px;font-size:13px;color:#000}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;max-width:315px;overflow-x:auto;white-space:nowrap}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-bottom:8px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end{display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .prefix,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .prefix{margin-left:5px;margin-right:5px;font-size:12px;color:#222}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .cron-description,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .cron-description{display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;margin-left:5px;font-size:10px;opacity:.5}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a{color:#000}.view-plugins-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-settings-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list .reboot{color:#333}.view-login main .main-container{position:relative}.view-login main .main-container .alert{position:absolute;top:0;left:0;right:0}.view-login main .main-container .login-content{display:flex;flex:1;flex-direction:column;justify-content:center;align-items:center}.view-login main .main-container .login-content .form-holder{width:400px;display:flex;justify-content:center;align-items:center;align-self:stretch;margin-left:auto;margin-right:auto}.view-login main .main-container .login-content .form-holder .card{display:flex;justify-content:center;align-items:center;align-self:stretch;border-radius:6px;padding:50px;color:#fff}.view-login main .main-container .login-content .form-holder .card form{padding:0;flex:1;display:flex;justify-content:center;align-items:center;align-self:stretch}.view-login main .main-container .login-content .form-holder .card form .actions{margin-top:10px}.view-login main .main-container .login-content .form-holder .card form .actions .btn{padding-left:20px;padding-right:20px}.view-auth-user-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item.disabled .tile-body{opacity:.3;text-decoration-line:line-through}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-tail a:last-child{margin-left:10px}menu h1.logo a{color:#666}menu:hover h1.logo a{color:transparent}ul.explr-dirview li a.with-thumbnail .img-holder{background:#ddd}ul.explr-dirview li a i{color:#027bff}button.btn-pixel,.btn.btn-pixel{background:#fff;color:#444;box-shadow:4px 0 #ccc,0 4px #11a948,-4px 0 #027bff,0 -4px #ef0e5d}button.btn-pixel:hover,.btn.btn-pixel:hover{box-shadow:6px 0 #ccc,0 6px #11a948,-6px 0 #027bff,0 -6px #ef0e5d}button.btn-naked,.btn.btn-naked{color:#777}button.btn-naked:hover,.btn.btn-naked:hover,button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #666 inset}.tiles .tiles-inner .tile-item{border-top:2px solid transparent;border-right:2px solid transparent;border-bottom:2px solid transparent}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-left:2px solid #E7E7E7;border-top:2px solid #E7E7E7;border-right:2px solid #E7E7E7;border-bottom:2px solid #E7E7E7;background:#fff}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-color:#027bff}.tiles .tiles-inner .tile-item:hover.starred,.tiles .tiles-inner .tile-item.active.starred{border-color:#ffa70a}.tiles .tiles-inner .tile-item:hover.disabled,.tiles .tiles-inner .tile-item.active.disabled{border-color:#bbb}.panes .pane-section .pane-item:nth-child(odd),.panes .pane-section tr:nth-child(odd),.panes tbody .pane-item:nth-child(odd),.panes tbody tr:nth-child(odd){background-color:#fff}.panes .pane-section .pane-item:nth-child(2n),.panes .pane-section tr:nth-child(2n),.panes tbody .pane-item:nth-child(2n),.panes tbody tr:nth-child(2n){background-color:#f7f7f7}.form-group .widget select,.form-group .widget input,.form-group .widget textarea{box-shadow:0 2px 1px #ddd,0 4px 2px #ddd inset;color:#555;background:#eee}.toggle label{box-shadow:0 2px 2px #ccc inset}.toggle label:after{box-shadow:0 2px #cccccce6}.toggle input:checked+label{box-shadow:0 2px 2px #0000004d inset}ul.pills{box-shadow:1px 1px .5px .5px inset #aaa3;background:#eee}ul.pills li a{color:#444;background:#fff}ul.pills li.active a{color:#fff;background:#027bff}.breadcrumb-container ul.breadcrumb{box-shadow:1px 1px .5px .5px inset #aaa3;background:#eee}.breadcrumb-container ul.breadcrumb li a,.breadcrumb-container ul.breadcrumb li span,.breadcrumb-container ul.breadcrumb li{color:#444}.breadcrumb-container ul.breadcrumb li:hover a{color:#fff}.breadcrumb-container ul.breadcrumb li.divider i{color:#bbb}.dropdown ul.dropdown-menu li.danger:hover a{color:#fff}.inner-empty i{color:#ddd;text-shadow:0 -1px #999,0 0px .5px #666}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a{color:#888}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a:hover,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a:hover{color:#fff}.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{border:none;background:#eee;border-radius:4px;padding-left:10px;padding-right:10px}.modal-explr-picker .explr-tree{background:#f7f7f7} +.warning{color:#e56723!important}.bg-warning{background-color:#e56723!important}.border-warning{border-color:#e56723!important}.info{color:#027bff!important}.bg-info{background-color:#027bff!important}.border-info{border-color:#027bff!important}.info-alt{color:#075cb7!important}.bg-info-alt{background-color:#075cb7!important}.border-info-alt{border-color:#075cb7!important}.success{color:#11a948!important}.bg-success{background-color:#11a948!important}.border-success{border-color:#11a948!important}.success-alt{color:#11a948!important}.bg-success-alt{background-color:#11a948!important}.border-success-alt{border-color:#11a948!important}.error{color:#ef0e5d!important}.bg-error{background-color:#ef0e5d!important}.border-error{border-color:#ef0e5d!important}.error-alt{color:#c20941!important}.bg-error-alt{background-color:#c20941!important}.border-error-alt{border-color:#c20941!important}.danger{color:#ef0e5d!important}.bg-danger{background-color:#ef0e5d!important}.border-danger{border-color:#ef0e5d!important}.danger-alt{color:#c20941!important}.bg-danger-alt{background-color:#c20941!important}.border-danger-alt{border-color:#c20941!important}.purple{color:#bc48ff!important}.bg-purple{background-color:#bc48ff!important}.border-purple{border-color:#bc48ff!important}.purple-alt{color:#692fbd!important}.bg-purple-alt{background-color:#692fbd!important}.border-purple-alt{border-color:#692fbd!important}.neutral{color:#464646!important}.bg-neutral{background-color:#464646!important}.border-neutral{border-color:#464646!important}.yellow{color:#ffa70a!important}.bg-yellow{background-color:#ffa70a!important}.border-yellow{border-color:#ffa70a!important}.white{color:#fff!important}.bg-white{background-color:#fff!important}.border-white{border-color:#fff!important}.black{color:#000!important}.bg-black{background-color:#000!important}.border-black{border-color:#000!important}.youtube{color:#fd3c01!important}.bg-youtube{background-color:#fd3c01!important}.border-youtube{border-color:#fd3c01!important}.raspbian{color:#b61240!important}.bg-raspbian{background-color:#b61240!important}.border-raspbian{border-color:#b61240!important}.windows{color:#12a7e3!important}.bg-windows{background-color:#12a7e3!important}.border-windows{border-color:#12a7e3!important}.macos{color:#b3bcc2!important}.bg-macos{background-color:#b3bcc2!important}.border-macos{border-color:#b3bcc2!important}.debian{color:#cf084e!important}.bg-debian{background-color:#cf084e!important}.border-debian{border-color:#cf084e!important}.fedora{color:#52a2da!important}.bg-fedora{background-color:#52a2da!important}.border-fedora{border-color:#52a2da!important}.ubuntu{color:#d64514!important}.bg-ubuntu{background-color:#d64514!important}.border-ubuntu{border-color:#d64514!important}.suse{color:#6fb425!important}.bg-suse{background-color:#6fb425!important}.border-suse{border-color:#6fb425!important}.redhat{color:#c60200!important}.bg-redhat{background-color:#c60200!important}.border-redhat{border-color:#c60200!important}.centos{color:#9b4c88!important}.bg-centos{background-color:#9b4c88!important}.border-centos{border-color:#9b4c88!important}.other{color:#ffa70a!important}.bg-other{background-color:#ffa70a!important}.border-other{border-color:#ffa70a!important}button.btn-warning,.btn.btn-warning{background:#e56723;box-shadow:0 2px #913e11}button.btn-warning:hover,.btn.btn-warning:hover{box-shadow:0 2px 0 1px #913e11 inset}button.btn-warning:focus,.btn.btn-warning:focus{background:#913e11}button.btn-wire-warning,.btn.btn-wire-warning{background:transparent;box-shadow:none;border:2px solid rgb(229,103,35);color:#000c}button.btn-wire-warning i.btn-match,.btn.btn-wire-warning i.btn-match{color:#e56723}button.btn-wire-warning:hover,.btn.btn-wire-warning:hover{background:#e567230d;border-color:#be5117;color:#be5117;box-shadow:none}button.btn-wire-warning:focus,.btn.btn-wire-warning:focus{border-color:#913e11;background:transparent}button.btn-info,.btn.btn-info{background:#027bff;box-shadow:0 2px #004a9b}button.btn-info:hover,.btn.btn-info:hover{box-shadow:0 2px 0 1px #004a9b inset}button.btn-info:focus,.btn.btn-info:focus{background:#004a9b}button.btn-wire-info,.btn.btn-wire-info{background:transparent;box-shadow:none;border:2px solid rgb(2,123,255);color:#000c}button.btn-wire-info i.btn-match,.btn.btn-wire-info i.btn-match{color:#027bff}button.btn-wire-info:hover,.btn.btn-wire-info:hover{background:#027bff0d;border-color:#0063ce;color:#0063ce;box-shadow:none}button.btn-wire-info:focus,.btn.btn-wire-info:focus{border-color:#004a9b;background:transparent}button.btn-info-alt,.btn.btn-info-alt{background:#075cb7;box-shadow:0 2px #032b55}button.btn-info-alt:hover,.btn.btn-info-alt:hover{box-shadow:0 2px 0 1px #032b55 inset}button.btn-info-alt:focus,.btn.btn-info-alt:focus{background:#032b55}button.btn-wire-info-alt,.btn.btn-wire-info-alt{background:transparent;box-shadow:none;border:2px solid rgb(7,92,183);color:#000c}button.btn-wire-info-alt i.btn-match,.btn.btn-wire-info-alt i.btn-match{color:#075cb7}button.btn-wire-info-alt:hover,.btn.btn-wire-info-alt:hover{background:#075cb70d;border-color:#054386;color:#054386;box-shadow:none}button.btn-wire-info-alt:focus,.btn.btn-wire-info-alt:focus{border-color:#032b55;background:transparent}button.btn-success,.btn.btn-success{background:#11a948;box-shadow:0 2px #084c21}button.btn-success:hover,.btn.btn-success:hover{box-shadow:0 2px 0 1px #084c21 inset}button.btn-success:focus,.btn.btn-success:focus{background:#084c21}button.btn-wire-success,.btn.btn-wire-success{background:transparent;box-shadow:none;border:2px solid rgb(17,169,72);color:#000c}button.btn-wire-success i.btn-match,.btn.btn-wire-success i.btn-match{color:#11a948}button.btn-wire-success:hover,.btn.btn-wire-success:hover{background:#11a9480d;border-color:#0c7b34;color:#0c7b34;box-shadow:none}button.btn-wire-success:focus,.btn.btn-wire-success:focus{border-color:#084c21;background:transparent}button.btn-success-alt,.btn.btn-success-alt{background:#11a948;box-shadow:0 2px #084c21}button.btn-success-alt:hover,.btn.btn-success-alt:hover{box-shadow:0 2px 0 1px #084c21 inset}button.btn-success-alt:focus,.btn.btn-success-alt:focus{background:#084c21}button.btn-wire-success-alt,.btn.btn-wire-success-alt{background:transparent;box-shadow:none;border:2px solid rgb(17,169,72);color:#000c}button.btn-wire-success-alt i.btn-match,.btn.btn-wire-success-alt i.btn-match{color:#11a948}button.btn-wire-success-alt:hover,.btn.btn-wire-success-alt:hover{background:#11a9480d;border-color:#0c7b34;color:#0c7b34;box-shadow:none}button.btn-wire-success-alt:focus,.btn.btn-wire-success-alt:focus{border-color:#084c21;background:transparent}button.btn-error,.btn.btn-error{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-error:hover,.btn.btn-error:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-error:focus,.btn.btn-error:focus{background:#8f0838}button.btn-wire-error,.btn.btn-wire-error{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#000c}button.btn-wire-error i.btn-match,.btn.btn-wire-error i.btn-match{color:#ef0e5d}button.btn-wire-error:hover,.btn.btn-wire-error:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-error:focus,.btn.btn-wire-error:focus{border-color:#8f0838;background:transparent}button.btn-error-alt,.btn.btn-error-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-error-alt:hover,.btn.btn-error-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-error-alt:focus,.btn.btn-error-alt:focus{background:#610420}button.btn-wire-error-alt,.btn.btn-wire-error-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#000c}button.btn-wire-error-alt i.btn-match,.btn.btn-wire-error-alt i.btn-match{color:#c20941}button.btn-wire-error-alt:hover,.btn.btn-wire-error-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-error-alt:focus,.btn.btn-wire-error-alt:focus{border-color:#610420;background:transparent}button.btn-danger,.btn.btn-danger{background:#ef0e5d;box-shadow:0 2px #8f0838}button.btn-danger:hover,.btn.btn-danger:hover{box-shadow:0 2px 0 1px #8f0838 inset}button.btn-danger:focus,.btn.btn-danger:focus{background:#8f0838}button.btn-wire-danger,.btn.btn-wire-danger{background:transparent;box-shadow:none;border:2px solid rgb(239,14,93);color:#000c}button.btn-wire-danger i.btn-match,.btn.btn-wire-danger i.btn-match{color:#ef0e5d}button.btn-wire-danger:hover,.btn.btn-wire-danger:hover{background:#ef0e5d0d;border-color:#bf0b4a;color:#bf0b4a;box-shadow:none}button.btn-wire-danger:focus,.btn.btn-wire-danger:focus{border-color:#8f0838;background:transparent}button.btn-danger-alt,.btn.btn-danger-alt{background:#c20941;box-shadow:0 2px #610420}button.btn-danger-alt:hover,.btn.btn-danger-alt:hover{box-shadow:0 2px 0 1px #610420 inset}button.btn-danger-alt:focus,.btn.btn-danger-alt:focus{background:#610420}button.btn-wire-danger-alt,.btn.btn-wire-danger-alt{background:transparent;box-shadow:none;border:2px solid rgb(194,9,65);color:#000c}button.btn-wire-danger-alt i.btn-match,.btn.btn-wire-danger-alt i.btn-match{color:#c20941}button.btn-wire-danger-alt:hover,.btn.btn-wire-danger-alt:hover{background:#c209410d;border-color:#910731;color:#910731;box-shadow:none}button.btn-wire-danger-alt:focus,.btn.btn-wire-danger-alt:focus{border-color:#610420;background:transparent}button.btn-purple,.btn.btn-purple{background:#bc48ff;box-shadow:0 2px #8f00e1}button.btn-purple:hover,.btn.btn-purple:hover{box-shadow:0 2px 0 1px #8f00e1 inset}button.btn-purple:focus,.btn.btn-purple:focus{background:#8f00e1}button.btn-wire-purple,.btn.btn-wire-purple{background:transparent;box-shadow:none;border:2px solid rgb(188,72,255);color:#000c}button.btn-wire-purple i.btn-match,.btn.btn-wire-purple i.btn-match{color:#bc48ff}button.btn-wire-purple:hover,.btn.btn-wire-purple:hover{background:#bc48ff0d;border-color:#a915ff;color:#a915ff;box-shadow:none}button.btn-wire-purple:focus,.btn.btn-wire-purple:focus{border-color:#8f00e1;background:transparent}button.btn-purple-alt,.btn.btn-purple-alt{background:#692fbd;box-shadow:0 2px #3c1b6b}button.btn-purple-alt:hover,.btn.btn-purple-alt:hover{box-shadow:0 2px 0 1px #3c1b6b inset}button.btn-purple-alt:focus,.btn.btn-purple-alt:focus{background:#3c1b6b}button.btn-wire-purple-alt,.btn.btn-wire-purple-alt{background:transparent;box-shadow:none;border:2px solid rgb(105,47,189);color:#000c}button.btn-wire-purple-alt i.btn-match,.btn.btn-wire-purple-alt i.btn-match{color:#692fbd}button.btn-wire-purple-alt:hover,.btn.btn-wire-purple-alt:hover{background:#692fbd0d;border-color:#522594;color:#522594;box-shadow:none}button.btn-wire-purple-alt:focus,.btn.btn-wire-purple-alt:focus{border-color:#3c1b6b;background:transparent}button.btn-neutral,.btn.btn-neutral{background:#464646;box-shadow:0 2px #131313}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #131313 inset}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#131313}button.btn-wire-neutral,.btn.btn-wire-neutral{background:transparent;box-shadow:none;border:2px solid rgb(70,70,70);color:#000c}button.btn-wire-neutral i.btn-match,.btn.btn-wire-neutral i.btn-match{color:#464646}button.btn-wire-neutral:hover,.btn.btn-wire-neutral:hover{background:#4646460d;border-color:#2d2d2d;color:#2d2d2d;box-shadow:none}button.btn-wire-neutral:focus,.btn.btn-wire-neutral:focus{border-color:#131313;background:transparent}button.btn-yellow,.btn.btn-yellow{background:#ffa70a;box-shadow:0 2px #a36800}button.btn-yellow:hover,.btn.btn-yellow:hover{box-shadow:0 2px 0 1px #a36800 inset}button.btn-yellow:focus,.btn.btn-yellow:focus{background:#a36800}button.btn-wire-yellow,.btn.btn-wire-yellow{background:transparent;box-shadow:none;border:2px solid rgb(255,167,10);color:#000c}button.btn-wire-yellow i.btn-match,.btn.btn-wire-yellow i.btn-match{color:#ffa70a}button.btn-wire-yellow:hover,.btn.btn-wire-yellow:hover{background:#ffa70a0d;border-color:#d68900;color:#d68900;box-shadow:none}button.btn-wire-yellow:focus,.btn.btn-wire-yellow:focus{border-color:#a36800;background:transparent}button.btn-white,.btn.btn-white{background:#fff;box-shadow:0 2px #ccc}button.btn-white:hover,.btn.btn-white:hover{box-shadow:0 2px 0 1px #ccc inset}button.btn-white:focus,.btn.btn-white:focus{background:#ccc}button.btn-wire-white,.btn.btn-wire-white{background:transparent;box-shadow:none;border:2px solid rgb(255,255,255);color:#000c}button.btn-wire-white i.btn-match,.btn.btn-wire-white i.btn-match{color:#fff}button.btn-wire-white:hover,.btn.btn-wire-white:hover{background:#ffffff0d;border-color:#e6e6e6;color:#e6e6e6;box-shadow:none}button.btn-wire-white:focus,.btn.btn-wire-white:focus{border-color:#ccc;background:transparent}button.btn-black,.btn.btn-black{background:#000;box-shadow:0 2px #000}button.btn-black:hover,.btn.btn-black:hover{box-shadow:0 2px 0 1px #000 inset}button.btn-black:focus,.btn.btn-black:focus{background:#000}button.btn-wire-black,.btn.btn-wire-black{background:transparent;box-shadow:none;border:2px solid rgb(0,0,0);color:#000c}button.btn-wire-black i.btn-match,.btn.btn-wire-black i.btn-match{color:#000}button.btn-wire-black:hover,.btn.btn-wire-black:hover{background:#0000000d;border-color:#000;color:#000;box-shadow:none}button.btn-wire-black:focus,.btn.btn-wire-black:focus{border-color:#000;background:transparent}button.btn-youtube,.btn.btn-youtube{background:#fd3c01;box-shadow:0 2px #972401}button.btn-youtube:hover,.btn.btn-youtube:hover{box-shadow:0 2px 0 1px #972401 inset}button.btn-youtube:focus,.btn.btn-youtube:focus{background:#972401}button.btn-wire-youtube,.btn.btn-wire-youtube{background:transparent;box-shadow:none;border:2px solid rgb(253,60,1);color:#000c}button.btn-wire-youtube i.btn-match,.btn.btn-wire-youtube i.btn-match{color:#fd3c01}button.btn-wire-youtube:hover,.btn.btn-wire-youtube:hover{background:#fd3c010d;border-color:#ca3001;color:#ca3001;box-shadow:none}button.btn-wire-youtube:focus,.btn.btn-wire-youtube:focus{border-color:#972401;background:transparent}button.btn-raspbian,.btn.btn-raspbian{background:#b61240;box-shadow:0 2px #59091f}button.btn-raspbian:hover,.btn.btn-raspbian:hover{box-shadow:0 2px 0 1px #59091f inset}button.btn-raspbian:focus,.btn.btn-raspbian:focus{background:#59091f}button.btn-wire-raspbian,.btn.btn-wire-raspbian{background:transparent;box-shadow:none;border:2px solid rgb(182,18,64);color:#000c}button.btn-wire-raspbian i.btn-match,.btn.btn-wire-raspbian i.btn-match{color:#b61240}button.btn-wire-raspbian:hover,.btn.btn-wire-raspbian:hover{background:#b612400d;border-color:#880d30;color:#880d30;box-shadow:none}button.btn-wire-raspbian:focus,.btn.btn-wire-raspbian:focus{border-color:#59091f;background:transparent}button.btn-windows,.btn.btn-windows{background:#12a7e3;box-shadow:0 2px #0b6184}button.btn-windows:hover,.btn.btn-windows:hover{box-shadow:0 2px 0 1px #0b6184 inset}button.btn-windows:focus,.btn.btn-windows:focus{background:#0b6184}button.btn-wire-windows,.btn.btn-wire-windows{background:transparent;box-shadow:none;border:2px solid rgb(18,167,227);color:#000c}button.btn-wire-windows i.btn-match,.btn.btn-wire-windows i.btn-match{color:#12a7e3}button.btn-wire-windows:hover,.btn.btn-wire-windows:hover{background:#12a7e30d;border-color:#0e84b4;color:#0e84b4;box-shadow:none}button.btn-wire-windows:focus,.btn.btn-wire-windows:focus{border-color:#0b6184;background:transparent}button.btn-macos,.btn.btn-macos{background:#b3bcc2;box-shadow:0 2px #7a8a95}button.btn-macos:hover,.btn.btn-macos:hover{box-shadow:0 2px 0 1px #7a8a95 inset}button.btn-macos:focus,.btn.btn-macos:focus{background:#7a8a95}button.btn-wire-macos,.btn.btn-wire-macos{background:transparent;box-shadow:none;border:2px solid rgb(179,188,194);color:#000c}button.btn-wire-macos i.btn-match,.btn.btn-wire-macos i.btn-match{color:#b3bcc2}button.btn-wire-macos:hover,.btn.btn-wire-macos:hover{background:#b3bcc20d;border-color:#97a3ab;color:#97a3ab;box-shadow:none}button.btn-wire-macos:focus,.btn.btn-wire-macos:focus{border-color:#7a8a95;background:transparent}button.btn-debian,.btn.btn-debian{background:#cf084e;box-shadow:0 2px #6d0429}button.btn-debian:hover,.btn.btn-debian:hover{box-shadow:0 2px 0 1px #6d0429 inset}button.btn-debian:focus,.btn.btn-debian:focus{background:#6d0429}button.btn-wire-debian,.btn.btn-wire-debian{background:transparent;box-shadow:none;border:2px solid rgb(207,8,78);color:#000c}button.btn-wire-debian i.btn-match,.btn.btn-wire-debian i.btn-match{color:#cf084e}button.btn-wire-debian:hover,.btn.btn-wire-debian:hover{background:#cf084e0d;border-color:#9e063b;color:#9e063b;box-shadow:none}button.btn-wire-debian:focus,.btn.btn-wire-debian:focus{border-color:#6d0429;background:transparent}button.btn-fedora,.btn.btn-fedora{background:#52a2da;box-shadow:0 2px #236ea3}button.btn-fedora:hover,.btn.btn-fedora:hover{box-shadow:0 2px 0 1px #236ea3 inset}button.btn-fedora:focus,.btn.btn-fedora:focus{background:#236ea3}button.btn-wire-fedora,.btn.btn-wire-fedora{background:transparent;box-shadow:none;border:2px solid rgb(82,162,218);color:#000c}button.btn-wire-fedora i.btn-match,.btn.btn-wire-fedora i.btn-match{color:#52a2da}button.btn-wire-fedora:hover,.btn.btn-wire-fedora:hover{background:#52a2da0d;border-color:#2c8bcd;color:#2c8bcd;box-shadow:none}button.btn-wire-fedora:focus,.btn.btn-wire-fedora:focus{border-color:#236ea3;background:transparent}button.btn-ubuntu,.btn.btn-ubuntu{background:#d64514;box-shadow:0 2px #79270b}button.btn-ubuntu:hover,.btn.btn-ubuntu:hover{box-shadow:0 2px 0 1px #79270b inset}button.btn-ubuntu:focus,.btn.btn-ubuntu:focus{background:#79270b}button.btn-wire-ubuntu,.btn.btn-wire-ubuntu{background:transparent;box-shadow:none;border:2px solid rgb(214,69,20);color:#000c}button.btn-wire-ubuntu i.btn-match,.btn.btn-wire-ubuntu i.btn-match{color:#d64514}button.btn-wire-ubuntu:hover,.btn.btn-wire-ubuntu:hover{background:#d645140d;border-color:#a73610;color:#a73610;box-shadow:none}button.btn-wire-ubuntu:focus,.btn.btn-wire-ubuntu:focus{border-color:#79270b;background:transparent}button.btn-suse,.btn.btn-suse{background:#6fb425;box-shadow:0 2px #3b5f14}button.btn-suse:hover,.btn.btn-suse:hover{box-shadow:0 2px 0 1px #3b5f14 inset}button.btn-suse:focus,.btn.btn-suse:focus{background:#3b5f14}button.btn-wire-suse,.btn.btn-wire-suse{background:transparent;box-shadow:none;border:2px solid rgb(111,180,37);color:#000c}button.btn-wire-suse i.btn-match,.btn.btn-wire-suse i.btn-match{color:#6fb425}button.btn-wire-suse:hover,.btn.btn-wire-suse:hover{background:#6fb4250d;border-color:#558a1c;color:#558a1c;box-shadow:none}button.btn-wire-suse:focus,.btn.btn-wire-suse:focus{border-color:#3b5f14;background:transparent}button.btn-redhat,.btn.btn-redhat{background:#c60200;box-shadow:0 2px #600100}button.btn-redhat:hover,.btn.btn-redhat:hover{box-shadow:0 2px 0 1px #600100 inset}button.btn-redhat:focus,.btn.btn-redhat:focus{background:#600100}button.btn-wire-redhat,.btn.btn-wire-redhat{background:transparent;box-shadow:none;border:2px solid rgb(198,2,0);color:#000c}button.btn-wire-redhat i.btn-match,.btn.btn-wire-redhat i.btn-match{color:#c60200}button.btn-wire-redhat:hover,.btn.btn-wire-redhat:hover{background:#c602000d;border-color:#930100;color:#930100;box-shadow:none}button.btn-wire-redhat:focus,.btn.btn-wire-redhat:focus{border-color:#600100;background:transparent}button.btn-centos,.btn.btn-centos{background:#9b4c88;box-shadow:0 2px #572a4c}button.btn-centos:hover,.btn.btn-centos:hover{box-shadow:0 2px 0 1px #572a4c inset}button.btn-centos:focus,.btn.btn-centos:focus{background:#572a4c}button.btn-wire-centos,.btn.btn-wire-centos{background:transparent;box-shadow:none;border:2px solid rgb(155,76,136);color:#000c}button.btn-wire-centos i.btn-match,.btn.btn-wire-centos i.btn-match{color:#9b4c88}button.btn-wire-centos:hover,.btn.btn-wire-centos:hover{background:#9b4c880d;border-color:#793b6a;color:#793b6a;box-shadow:none}button.btn-wire-centos:focus,.btn.btn-wire-centos:focus{border-color:#572a4c;background:transparent}button.btn-other,.btn.btn-other{background:#ffa70a;box-shadow:0 2px #a36800}button.btn-other:hover,.btn.btn-other:hover{box-shadow:0 2px 0 1px #a36800 inset}button.btn-other:focus,.btn.btn-other:focus{background:#a36800}button.btn-wire-other,.btn.btn-wire-other{background:transparent;box-shadow:none;border:2px solid rgb(255,167,10);color:#000c}button.btn-wire-other i.btn-match,.btn.btn-wire-other i.btn-match{color:#ffa70a}button.btn-wire-other:hover,.btn.btn-wire-other:hover{background:#ffa70a0d;border-color:#d68900;color:#d68900;box-shadow:none}button.btn-wire-other:focus,.btn.btn-wire-other:focus{border-color:#a36800;background:transparent}.alert.alert-warning{color:#e56723;background:#e5672333}.alert.alert-info{color:#027bff;background:#027bff33}.alert.alert-info-alt{color:#075cb7;background:#075cb733}.alert.alert-success,.alert.alert-success-alt{color:#11a948;background:#11a94833}.alert.alert-error{color:#ef0e5d;background:#ef0e5d33}.alert.alert-error-alt{color:#c20941;background:#c2094133}.alert.alert-danger{color:#ef0e5d;background:#ef0e5d33}.alert.alert-danger-alt{color:#c20941;background:#c2094133}.alert.alert-purple{color:#bc48ff;background:#bc48ff33}.alert.alert-purple-alt{color:#692fbd;background:#692fbd33}.alert.alert-neutral{color:#464646;background:#46464633}.alert.alert-yellow{color:#ffa70a;background:#ffa70a33}.alert.alert-white{color:#fff;background:#fff3}.alert.alert-black{color:#000;background:#0003}.alert.alert-youtube{color:#fd3c01;background:#fd3c0133}.alert.alert-raspbian{color:#b61240;background:#b6124033}.alert.alert-windows{color:#12a7e3;background:#12a7e333}.alert.alert-macos{color:#b3bcc2;background:#b3bcc233}.alert.alert-debian{color:#cf084e;background:#cf084e33}.alert.alert-fedora{color:#52a2da;background:#52a2da33}.alert.alert-ubuntu{color:#d64514;background:#d6451433}.alert.alert-suse{color:#6fb425;background:#6fb42533}.alert.alert-redhat{color:#c60200;background:#c6020033}.alert.alert-centos{color:#9b4c88;background:#9b4c8833}.alert.alert-other{color:#ffa70a;background:#ffa70a33}@font-face{font-family:Sixtyfour;src:url(../../webfonts/Sixtyfour-Regular.ttf) format("truetype")}*{font-family:Roboto,Arial,"sans-serif";margin:0;padding:0;box-sizing:border-box}html{background-color:#fff}body,html{height:100%;font-family:Arial,sans-serif}.container{display:flex;height:100vh}.horizontal{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;align-self:stretch}main{flex:1;display:flex;flex-direction:column}main .main-container{display:flex;flex-direction:column;flex:1;overflow:hidden;align-self:stretch}main .main-container .top-content{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:10px 10px 10px 15px;background:transparent;border-bottom:1px solid #DDD}main .main-container .top-content h1{color:#000;font-weight:600;font-size:24px}main .main-container .top-content .top-actions{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}main .main-container .top-content .top-actions.align-right{justify-content:flex-end;margin-right:10px}main .main-container .top-content .top-actions .btn:first-child{margin-left:0!important}main .main-container .top-content .top-actions .btn,main .main-container .top-content .top-actions button{margin-left:10px}main .main-container .bottom-content{display:flex;flex-direction:row;align-self:stretch;justify-content:flex-start;align-items:flex-start;flex:1;overflow-y:auto;background:radial-gradient(circle at 0% 53%,rgba(2,123,255,.8) 10%,transparent 45%),radial-gradient(circle at 135% 53%,rgba(2,123,255,.8) 10%,transparent 95%),radial-gradient(circle at 50% 80%,rgba(105,47,189,.8) 40%,transparent 95%)}main .main-container .bottom-content .page-content{flex:2;overflow-y:auto;align-self:stretch;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;background:#fffc;padding:5px}main .main-container .bottom-content .page-content .inner{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;padding:10px 10px 40px;background:#fff;align-self:stretch}main .main-container .bottom-content .page-content .inner h1,main .main-container .bottom-content .page-content .inner h2,main .main-container .bottom-content .page-content .inner h3,main .main-container .bottom-content .page-content .inner h4,main .main-container .bottom-content .page-content .inner h5,main .main-container .bottom-content .page-content .inner h6{color:#222}main .main-container .bottom-content .page-content .inner p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;color:#999}main .main-container .bottom-content .page-panel{flex:1;overflow-y:auto;align-self:stretch;background:#fff;border-top:none}main .main-container .bottom-content .page-panel.left-panel{border-right:1px solid #DDD;border-left:none}main .main-container .bottom-content .page-panel.left-panel.explr-explorer{flex:.5;overflow-y:auto;padding:0;background:#fff;box-shadow:1px 1px .5px .5px inset #fff3;max-width:250px}main .main-container .bottom-content .page-panel.right-panel{border-left:1px solid #DDD;border-right:none}.invisible{visibility:hidden!important}.hidden{display:none!important}.tac{text-align:center!important}.tar{text-align:right!important}a{text-decoration:none}.normal{font-weight:400!important}.bold{font-weight:700!important}.col{display:flex;flex:1;flex-direction:column;align-self:stretch}main .context-bar{padding:10px;position:sticky;top:0;z-index:1000;max-height:80px;border-bottom:1px solid #DDD;display:flex;flex-direction:row;align-items:center}main .context-bar .context-menu{flex:1}main .context-bar .context-menu .inner{display:flex}main .context-bar .context-menu .inner ul.pills{margin:0}main .context-bar .context-divider{width:1px;height:100%;background:#ddd;margin-left:20px;margin-right:20px}main .context-bar .contex-tail{margin-right:20px}main .context-bar .contex-tail .btn{margin-right:0}main .context-bar .context-user{display:flex;margin-right:20px}main .context-bar .context-user .trigger{color:#000}main .context-bar .context-user .trigger .avatar{width:32px;height:32px;border-radius:4px;background:#555;margin-right:10px;display:flex;flex-direction:row;justify-content:center;align-items:center;text-align:center;font-weight:700;font-size:14px;border:1px solid #BBB;color:#fff}main .context-bar .context-user .trigger i{margin-top:-5px;margin-left:10px}menu{width:300px;background:#fff;overflow-y:auto;overflow-x:visible;padding:20px;z-index:2000;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;border-right:1px solid #DDD;min-width:64px}menu h1.logo{margin:40px 0 0 10px;align-self:stretch;display:flex}menu h1.logo a{text-align:center;text-shadow:0px 0 0 rgb(255,255,255),0px 2px 0 #BBB,0 0px 0 rgb(17,169,72),-0px 0 0 rgb(2,123,255),0 -0px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent;flex:1;font-family:Sixtyfour,Work Sans,Arial,"sans-serif";align-self:stretch;padding-right:3px;font-size:20px;text-transform:uppercase;transition:all .55s cubic-bezier(.19,1,.22,1);display:flex;flex-direction:row;justify-content:center;align-items:center;position:relative;color:#fff}menu h1.logo a img{flex-shrink:0;width:30px;margin-right:10px;position:absolute;left:5px;transition:all .55s cubic-bezier(.19,1,.22,1)}menu h1.logo a img.after{opacity:0}menu:hover h1.logo a{text-align:center;text-shadow:3px 0 0 rgb(255,255,255),3px 2px 0 #BBB,0 3px 0 rgb(17,169,72),-3px 0 0 rgb(2,123,255),0 -3px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}menu:hover h1.logo a img.before{opacity:0}menu:hover h1.logo a img.after{animation-duration:.2s;animation-name:logotouch}menu nav{display:flex;align-self:stretch;flex:1}menu nav ul{margin:60px 0 20px;flex:1;align-self:flex-start;display:flex;flex-direction:column;list-style:none}menu nav ul li{align-self:stretch;overflow:hidden;position:relative;transition:all .55s cubic-bezier(.19,1,.22,1);margin:10px 0;border-radius:4px}menu nav ul li a{color:#000000e6;font-size:16px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1;padding-top:5px;padding-bottom:5px;padding-left:10px}menu nav ul li a i{color:#000;opacity:.2;background:transparent;display:flex;justify-content:center;align-items:center;align-self:stretch;padding:10px;width:40px;border-radius:4px;text-align:center;margin-right:20px}menu nav ul li:after{background:#000;content:"";height:195px;left:-200px;opacity:.2;position:absolute;top:-50px;transform:rotate(35deg);transition:all .55s cubic-bezier(.19,1,.22,1);width:50px;z-index:-2;cursor:pointer}menu nav ul li.active a{color:#027bff;font-weight:700}menu nav ul li.active a i{opacity:1;color:#fff;background:#000000e6;background:#027bff}menu nav ul li:hover{background:#027bff}menu nav ul li:hover:after{z-index:2;left:120%;transition:all .55s cubic-bezier(.19,1,.22,1)}menu nav ul li:hover a{color:#fff;font-weight:700}menu nav ul li:hover a i{color:#fff;opacity:1}menu footer{background:#00000003;padding:20px 0;display:flex;flex-direction:row;align-self:stretch;text-align:center;justify-content:center}menu footer p{color:#bbb}menu footer p.version a{color:#888;font-weight:700}.dropdown{position:relative;display:flex;align-self:stretch}.dropdown.dropdown-show ul.dropdown-menu{display:flex;flex-direction:column}.dropdown .trigger{cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;flex:1}.dropdown ul.dropdown-menu{position:absolute;top:100%;left:0;display:none;background-color:#ddd;box-shadow:0 8px 16px #fff3;z-index:1000;list-style-type:none;margin:0;overflow:hidden;border-radius:4px}.dropdown ul.dropdown-menu li{padding:8px 16px;cursor:pointer;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;transition:all .55s cubic-bezier(.19,1,.22,1)}.dropdown ul.dropdown-menu li.danger:hover{background-color:#ef0e5d}.dropdown ul.dropdown-menu li:hover{background-color:#027bff}.dropdown ul.dropdown-menu li a{padding:8px 16px 8px 8px;color:#000;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch}.dropdown ul.dropdown-menu li a i{margin-right:15px}@keyframes logotouch{0%{opacity:0}50%{opacity:1}to{opacity:0;left:27px}}@keyframes shakednd{0%{transform:rotate(-2deg)}to{transform:rotate(2deg)}}button,.btn{position:relative;padding:10px 10px 8px;font-size:14px;color:#fff;cursor:pointer;border:none;border-radius:4px;background:#027bff;box-shadow:0 2px #004a9b;font-weight:700;letter-spacing:-.5px;margin-top:-2px;min-width:38px;min-height:34px;text-align:center;justify-content:center}button i.icon-left,.btn i.icon-left{margin-right:5px}button:hover,.btn:hover{box-shadow:0 2px 0 1px #004a9b inset;color:#fffc}button:focus,.btn:focus{background:#004a9b;color:#ffffff80;box-shadow:none}button.btn-pixel,.btn.btn-pixel{background:#333;border:1px solid transparent;transition:all .55s cubic-bezier(.19,1,.22,1);text-transform:uppercase;font-size:12px;box-shadow:4px 0 #fff,0 4px #11a948,-4px 0 #027bff,0 -4px #ef0e5d;color:#ddd;overflow:hidden}button.btn-pixel:hover,.btn.btn-pixel:hover{box-shadow:6px 0 #fff,0 6px #11a948,-6px 0 #027bff,0 -6px #ef0e5d;text-align:center;text-shadow:4px 0 0 rgb(255,255,255),4px 2px 0 #BBB,0 4px 0 rgb(17,169,72),-4px 0 0 rgb(2,123,255),0 -4px 0 rgb(239,14,93);text-decoration:none;background:linear-gradient(90deg,#a0a0a0 0,#bebebe 46%,#dcdcdc);-webkit-background-clip:text;color:transparent}button.btn-neutral,.btn.btn-neutral{color:#fff;background:#aaa;box-shadow:0 2px #919191;border:1px solid transparent}button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #222 inset;background:#919191}button.btn-neutral:focus,.btn.btn-neutral:focus{background:#777;border:1px solid #555}button .btn-wire-neutral,.btn .btn-wire-neutral{background:transparent;border:2px solid #AAA;color:#fffc;box-shadow:none}button .btn-wire-neutral:hover,.btn .btn-wire-neutral:hover{background:#aaaaaa0d;border-color:#919191;color:#919191;box-shadow:none}button .btn-wire-neutral:focus,.btn .btn-wire-neutral:focus{border-color:#777;background:transparent}button.btn-naked,.btn.btn-naked{background:transparent;box-shadow:none;border:1px solid transparent;color:#000}button.btn-naked:hover,.btn.btn-naked:hover{box-shadow:0 2px 0 1px #222 inset;background:#919191;color:#fff}button.btn-naked:focus,.btn.btn-naked:focus{background:#777;border:1px solid #555}button i.main,.btn i.main{font-size:18px}button sup,button sub,.btn sup,.btn sub{position:absolute;top:-4px;right:-4px;background:#888;border-bottom:2px solid #AAA;color:red;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}button sup i,button sub i,.btn sup i,.btn sub i{color:#fff;font-size:10px}button.btn-double-icon,.btn.btn-double-icon{margin-right:5px}button.disabled,.btn.disabled{cursor:default}.alert{padding:20px;align-self:stretch;display:flex;flex-direction:row;justify-content:center;align-items:center;border-radius:4px}.alert i{margin-right:13px}.alert a{color:inherit;margin-left:4px;margin-right:4px;text-decoration:underline}ul.explr-tree{height:100%!important}ul.explr-tree ul{padding-top:0!important}ul.explr-tree li{position:relative}ul.explr-tree li span{color:#aaa;font-size:13px;padding-left:5px;cursor:pointer}ul.explr-tree li span.explr-plus,ul.explr-tree li span.explr-minus{z-index:1}ul.explr-tree li span.explr-plus:hover,ul.explr-tree li span.explr-minus:hover{color:#555}ul.explr-tree li i.main{font-size:14px}ul.explr-tree li sup,ul.explr-tree li sub{position:absolute;top:0;left:5px;background:#888;border-bottom:2px solid #AAA;border-radius:4px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-tree li sup i,ul.explr-tree li sub i{color:#fff;font-size:4px!important;margin-bottom:0}ul.explr-tree li a{color:#000;padding-right:80px;margin-top:2px}ul.explr-tree li a:hover{color:#000}ul.explr-tree li a.active{background:#0000001a;border-radius:4px;font-weight:700;text-decoration:underline;margin-left:35px;padding-left:5px;margin-right:10px}.explr-multiselection-actions,.explr-selection-actions{display:none;flex-direction:row;justify-content:flex-end;align-items:center;flex:1}.explr-multiselection-actions button,.explr-selection-actions button{display:none}body.explr-selection-actionable .explr-selection-actions,body.explr-selection-actionable.explr-selection-folder .explr-selection-actions button.explr-selection-folder,body.explr-selection-actionable.explr-selection-entity .explr-selection-actions button.explr-selection-entity,body.explr-multiselection-actionable .explr-multiselection-actions,body.explr-multiselection-actionable.explr-multiselection-folder .explr-multiselection-actions button.explr-multiselection-folder,body.explr-multiselection-actionable.explr-multiselection-entity .explr-multiselection-actions button.explr-multiselection-entity{display:flex}.selectable-zone{flex:1;align-self:stretch;border:1px solid transparent}ul.explr-dirview{display:flex;flex-direction:row;flex-wrap:wrap}ul.explr-dirview li{display:flex;flex-direction:column;justify-content:flex-start;align-items:center;flex-shrink:0;margin:10px;min-width:100px;min-height:130px;padding-top:5px;border:1px solid transparent;border-radius:4px}ul.explr-dirview li.renaming a span{display:none}ul.explr-dirview li.renaming a form{display:block}ul.explr-dirview li.highlight-drop{border:1px dotted rgba(2,123,255,.4);background:#027bff4d}ul.explr-dirview li.highlight-clicked{border:1px dotted rgba(0,0,0,.2);background:#0000001a}ul.explr-dirview li a{color:#444;text-decoration:none;flex:1;text-align:center;font-size:12px;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;max-width:84px;min-width:84px;position:relative;word-break:break-all}ul.explr-dirview li a.with-thumbnail .img-holder{width:64px;height:64px;background:#000;border-radius:8px;display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden;margin-bottom:12px}ul.explr-dirview li a.with-thumbnail .img-holder img{max-height:100%;max-width:100%}ul.explr-dirview li a.with-thumbnail i{font-size:24px;position:absolute;top:-4px;left:-4px;text-shadow:0 .5px .5px #888}ul.explr-dirview li a i{font-size:64px;margin-bottom:12px;border-radius:8px}ul.explr-dirview li a sup,ul.explr-dirview li a sub{position:absolute;top:-2px;right:0;background:#888;border-bottom:2px solid #AAA;border-radius:4px;width:16px;height:16px;text-align:center;display:flex;justify-content:center;align-items:center;padding-top:2px;padding-bottom:1px}ul.explr-dirview li a sup i,ul.explr-dirview li a sub i{color:#fff;font-size:10px;margin-bottom:0}ul.explr-dirview li a input{width:100%;padding:0 3px}ul.explr-dirview li a input:focus{outline:none}ul.explr-dirview li a:hover{opacity:.8}ul.explr-dirview li a form{display:none}ul.explr-dirview li.new-folder a{color:#027bff}ul.explr-dirview li.new-folder a form{display:block}ul.explr-dirview .ui-draggable-dragging{z-index:20}ul.explr-dirview .ui-draggable-dragging a{opacity:1!important}.modal-explr-picker h2{margin-top:0}.modal-explr-picker .explr-tree{width:400px;max-width:400px;max-height:300px;overflow:auto;background:#ddd;padding:10px 20px;border-radius:4px;margin-top:15px}.content-explr-picker{cursor:pointer}.selection-rectangle{position:absolute;border:1px solid rgb(0,153,255);background-color:#0099ff1a;pointer-events:none;z-index:1000}ul.pills{background:#ddd;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #fff3;border:1px solid #DDD;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0}ul.pills li.divider{margin:0 20px;width:1px;height:100%;background:#ccc}ul.pills li:hover a{opacity:.9}ul.pills li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#000;overflow:hidden;padding-right:30px;text-align:center;background:#fff3;margin-right:5px;transition:all .25s cubic-bezier(.19,1,.22,1)}ul.pills li a span{display:flex;justify-content:center;align-items:center;margin-right:20px;height:42px;background:#fff3;width:42px}ul.pills li.active a{color:#ccc;background:#333;font-weight:700}ul.pills li:hover a{color:#fff;background:#027bff}ul.pills li:last-child a{margin-right:0}.breadcrumb-container{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;border-bottom:1px solid #DDD;background:transparent;padding:10px}.breadcrumb-container ul.breadcrumb{background:#ddd;padding:6px 4px 5px;box-shadow:1px 1px .5px .5px inset #fff3;border:1px solid #DDD;border-radius:4px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;list-style:none;margin:0;overflow-x:auto;max-width:65vw;white-space:nowrap}.breadcrumb-container ul.breadcrumb li{display:inline-block}.breadcrumb-container ul.breadcrumb li.divider{margin:0 5px}.breadcrumb-container ul.breadcrumb li.divider i{color:#555}.breadcrumb-container ul.breadcrumb li span,.breadcrumb-container ul.breadcrumb li a{border-radius:4px;display:flex;flex-direction:row;justify-content:center;align-items:center;color:#000;text-align:center;padding:0 3px}.breadcrumb-container ul.breadcrumb li span i,.breadcrumb-container ul.breadcrumb li a i{margin-right:5px}.breadcrumb-container ul.breadcrumb li:hover a{color:#000;background:#027bff}.breadcrumb-container ul.breadcrumb li:last-child a{margin-right:0}.pickers,.modals{position:fixed;background:#0006;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:10000}.pickers.pickers .modals-outer .modals-inner .modal h2,.modals.pickers .modals-outer .modals-inner .modal h2{font-size:14px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.pickers .modals-outer,.modals .modals-outer{min-width:464px;max-width:464px;display:flex;flex-direction:column;overflow:auto;padding-bottom:2px}.pickers .modals-outer .modals-inner,.modals .modals-outer .modals-inner{background:#fff;border-radius:10px;color:#fff;padding:40px;box-shadow:0 2px #ddd;border:1px solid #DDD}.pickers .modals-outer .modals-inner .modal h2,.modals .modals-outer .modals-inner .modal h2{padding:0;margin:0 0 30px;font-weight:400;color:#666}.pickers .modals-outer .modals-inner .modal h3,.modals .modals-outer .modals-inner .modal h3{align-self:stretch;margin:0 0 10px;font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#000;padding-bottom:10px;text-decoration:none}.pickers .modals-outer .modals-inner .modal h3.divide,.modals .modals-outer .modals-inner .modal h3.divide{border-top:1px solid #DDD;margin-top:20px;padding-top:20px}.toast{visibility:hidden;min-width:250px;margin-left:-125px;background-color:#ccc;color:#000;text-align:center;border-radius:4px;padding:16px;position:fixed;z-index:1;left:50%;bottom:30px;font-size:17px;border:2px dashed #DDD}.toast.show{visibility:visible;-webkit-animation:fadein .5s,fadeout .5s 2.5s;animation:fadein .5s,fadeout .5s 2.5s}@-webkit-keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@keyframes fadein{0%{bottom:0;opacity:0}to{bottom:30px;opacity:1}}@-webkit-keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}@keyframes fadeout{0%{bottom:30px;opacity:1}to{bottom:0;opacity:0}}body.dragover .shakeondrag{animation:shakednd .1s linear alternate infinite}.btn-super-upload-busy,.btn-super-upload{display:flex;flex-direction:row;justify-content:center;align-items:center;margin-left:10px;position:relative}.btn-super-upload-busy.btn-super-upload-busy,.btn-super-upload.btn-super-upload-busy{border:none!important}.btn-super-upload-busy .unprogress,.btn-super-upload .unprogress{display:block}.btn-super-upload-busy .progress,.btn-super-upload .progress{display:none;width:200px;height:10px;background:#999;border-radius:4px;flex-direction:row;justify-content:flex-start;align-items:center}.btn-super-upload-busy .progress .progress-bar,.btn-super-upload .progress .progress-bar{border-radius:4px;background-color:#027bff;height:100%}.btn-super-upload-busy .progress .percent,.btn-super-upload .progress .percent{display:flex;justify-content:center;align-items:center;position:absolute;left:0;right:0;bottom:0;top:2px;font-size:15px;color:#fff;text-shadow:0 0 2px rgb(0,0,0)}.btn-super-upload-busy.uploading .progress,.btn-super-upload.uploading .progress{display:block}.btn-super-upload-busy.uploading .unprogress,.btn-super-upload.uploading .unprogress{display:none}.panes{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.panes .pane-section,.panes tbody{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:1px 1px 28px;background:#fff;border-radius:4px;border:4px solid rgba(0,0,0,.05)}.panes .pane-section:hover,.panes tbody:hover,.panes .pane-section:hover tr.title-item,.panes tbody:hover tr.title-item{border-color:#027bff0d}.panes .pane-section .pane-item,.panes .pane-section tr,.panes tbody .pane-item,.panes tbody tr{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;padding:8px 18px;background:#ddd}.panes .pane-section .pane-item:nth-child(odd),.panes .pane-section tr:nth-child(odd),.panes tbody .pane-item:nth-child(odd),.panes tbody tr:nth-child(odd){background-color:#eee}.panes .pane-section .pane-item:nth-child(odd) td.description,.panes .pane-section tr:nth-child(odd) td.description,.panes tbody .pane-item:nth-child(odd) td.description,.panes tbody tr:nth-child(odd) td.description{color:#000000b3}.panes .pane-section .pane-item:nth-child(2n),.panes .pane-section tr:nth-child(2n),.panes tbody .pane-item:nth-child(2n),.panes tbody tr:nth-child(2n){background-color:#e4e4e4}.panes .pane-section .pane-item .pane-cell,.panes .pane-section .pane-item td,.panes .pane-section tr .pane-cell,.panes .pane-section tr td,.panes tbody .pane-item .pane-cell,.panes tbody .pane-item td,.panes tbody tr .pane-cell,.panes tbody tr td{color:#777;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;flex:1;font-size:14px}.panes .pane-section .pane-item .pane-cell.vertical,.panes .pane-section .pane-item td.vertical,.panes .pane-section tr .pane-cell.vertical,.panes .pane-section tr td.vertical,.panes tbody .pane-item .pane-cell.vertical,.panes tbody .pane-item td.vertical,.panes tbody tr .pane-cell.vertical,.panes tbody tr td.vertical{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start}.panes .pane-section .pane-item .pane-cell.description,.panes .pane-section .pane-item td.description,.panes .pane-section tr .pane-cell.description,.panes .pane-section tr td.description,.panes tbody .pane-item .pane-cell.description,.panes tbody .pane-item td.description,.panes tbody tr .pane-cell.description,.panes tbody tr td.description{align-self:stretch}.panes .pane-section .pane-item .pane-cell.value,.panes .pane-section .pane-item td.value,.panes .pane-section tr .pane-cell.value,.panes .pane-section tr td.value,.panes tbody .pane-item .pane-cell.value,.panes tbody .pane-item td.value,.panes tbody tr .pane-cell.value,.panes tbody tr td.value{flex:0;margin-left:20px;word-break:break-all;flex-basis:auto}.panes .pane-section .pane-item .pane-cell.value i.icon-legend,.panes .pane-section .pane-item td.value i.icon-legend,.panes .pane-section tr .pane-cell.value i.icon-legend,.panes .pane-section tr td.value i.icon-legend,.panes tbody .pane-item .pane-cell.value i.icon-legend,.panes tbody .pane-item td.value i.icon-legend,.panes tbody tr .pane-cell.value i.icon-legend,.panes tbody tr td.value i.icon-legend{font-size:10px;margin-right:10px}.panes .pane-section .pane-item .pane-cell.value i.icon-value,.panes .pane-section .pane-item td.value i.icon-value,.panes .pane-section tr .pane-cell.value i.icon-value,.panes .pane-section tr td.value i.icon-value,.panes tbody .pane-item .pane-cell.value i.icon-value,.panes tbody .pane-item td.value i.icon-value,.panes tbody tr .pane-cell.value i.icon-value,.panes tbody tr td.value i.icon-value{padding:2px 2px 1px;border-radius:2px;width:16px;text-align:center}.panes .pane-section .pane-item.title-item,.panes .pane-section tr.title-item,.panes tbody .pane-item.title-item,.panes tbody tr.title-item{border-bottom:1px solid #DDD}.panes .pane-section .pane-item.title-item td,.panes .pane-section tr.title-item td,.panes tbody .pane-item.title-item td,.panes tbody tr.title-item td{color:#027bffe6;font-size:16px;font-weight:700}.panes .pane-section .pane-item.title-item td i,.panes .pane-section tr.title-item td i,.panes tbody .pane-item.title-item td i,.panes tbody tr.title-item td i{margin-right:10px}.panes .pane-section .pane-item.title-item td .more,.panes .pane-section tr.title-item td .more,.panes tbody .pane-item.title-item td .more,.panes tbody tr.title-item td .more{flex:1;text-align:right;font-size:12px;color:#777;font-weight:400;font-style:italic}.panes .pane-section .pane-item.variable-item,.panes .pane-section tr.variable-item,.panes tbody .pane-item.variable-item,.panes tbody tr.variable-item{cursor:pointer}.panes .pane-section .pane-item.variable-item:hover,.panes .pane-section tr.variable-item:hover,.panes tbody .pane-item.variable-item:hover,.panes tbody tr.variable-item:hover{background-color:#027bff0d}.panes .pane-section .pane-item.variable-item:hover td,.panes .pane-section tr.variable-item:hover td,.panes tbody .pane-item.variable-item:hover td,.panes tbody tr.variable-item:hover td{font-weight:700;color:#000}.panes .pane-section .pane-item.variable-item:hover td i.icon-legend,.panes .pane-section tr.variable-item:hover td i.icon-legend,.panes tbody .pane-item.variable-item:hover td i.icon-legend,.panes tbody tr.variable-item:hover td i.icon-legend{color:#000}.panes .pane-section .pane-item.variable-item:hover td span,.panes .pane-section .pane-item.variable-item:hover td i.icon-value,.panes .pane-section tr.variable-item:hover td span,.panes .pane-section tr.variable-item:hover td i.icon-value,.panes tbody .pane-item.variable-item:hover td span,.panes tbody .pane-item.variable-item:hover td i.icon-value,.panes tbody tr.variable-item:hover td span,.panes tbody tr.variable-item:hover td i.icon-value{background-color:#0000004d;color:#000}.panes .pane-section .pane-item.variable-item:hover td.description,.panes .pane-section tr.variable-item:hover td.description,.panes tbody .pane-item.variable-item:hover td.description,.panes tbody tr.variable-item:hover td.description{color:#027bff}.tiles{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;align-self:stretch}.tiles .tiles-inner{display:flex;flex:1;flex-direction:column;flex-wrap:nowrap;justify-content:flex-start;align-items:flex-start;align-self:stretch;padding:2px}.tiles .tiles-inner .tile-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#000;margin:1px;padding:15px 10px 15px 15px;border-radius:4px;border-bottom:1px solid transparent}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-left:4px solid rgb(2,123,255);border-radius:4px;border-bottom:2px solid #E7E7E7;background:#ddd;color:#027bff}.tiles .tiles-inner .tile-item:hover:hover,.tiles .tiles-inner .tile-item.active:hover{opacity:1}.tiles .tiles-inner .tile-item:hover.disabled,.tiles .tiles-inner .tile-item.active.disabled{border-left-color:#bbb;color:#000}.tiles .tiles-inner .tile-item:hover.starred,.tiles .tiles-inner .tile-item.active.starred{border-left-color:#ffa70a;color:#ffa70a}.tiles .tiles-inner .tile-item:hover.starred .tile-tail .head-icon i,.tiles .tiles-inner .tile-item.active.starred .tile-tail .head-icon i{opacity:1;color:#ffa70a;font-size:8px}.tiles .tiles-inner .tile-item.starred .tile-tail .head-icon i{font-size:8px;color:#ffa70a}.tiles .tiles-inner .tile-item.disabled .tile-body{opacity:.3}.tiles .tiles-inner .tile-item.disabled .tile-tail .head-icon i{color:#bbb;opacity:.4}.tiles .tiles-inner .tile-item .tile-tail{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin-left:10px;min-width:10px}.tiles .tiles-inner .tile-item .tile-tail .head-icon{flex:1;display:flex;text-align:right;flex-direction:row;justify-content:center;align-items:center;align-self:stretch}.tiles .tiles-inner .tile-item .tile-tail .head-icon i{font-size:6px;display:flex}.tiles .tiles-inner .tile-item .tile-tail .status-icons{display:flex;flex-direction:row;justify-content:flex-end;align-items:center}.tiles .tiles-inner .tile-item .tile-tail .status-icons i{font-size:16px;margin-left:10px}.tiles .tiles-inner .tile-item .tile-body{flex:1;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;font-size:15px;font-weight:400;letter-spacing:.8px;line-height:22px;margin:0;flex-wrap:nowrap}.tiles .tiles-inner .tile-item .tile-body i{font-size:8px;margin-right:5px}.tiles .tiles-inner .tile-item .tile-metrics{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;margin:0}.tiles .tiles-inner .tile-item .tile-metrics .foot-span span,.tiles .tiles-inner .tile-item .tile-metrics .foot-span{opacity:.8;font-size:13px;font-family:Courier New}span.empty{background:#ffa70a4d;color:#ffa70a;text-transform:lowercase;border-radius:2px;padding:2px 4px;font-weight:700}.inner-empty{display:flex;flex:1;flex-direction:column;align-self:stretch;justify-content:center;align-items:center}.inner-empty i{font-size:90px;opacity:.3;text-shadow:0 -1px #CCC,0 0px .5px #BBB}.toggle{position:relative;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.toggle input{display:none}.toggle input:checked+label{background:#027bff;border:1px solid rgba(0,0,0,.1);box-shadow:0 2px 2px #222 inset}.toggle input:checked+label:after{content:"";display:block;border-radius:50%;margin-left:21px;width:18px;height:18px;transition:.2s;background:#9bcbff;box-shadow:0 2px #0063ce}.toggle label{width:44px;height:26px;border-radius:15px;background:#ddd;cursor:pointer;border:1px solid rgba(0,0,0,.1);box-shadow:0 2px 2px #111 inset}.toggle label:after{content:"";display:block;border-radius:50%;width:18px;height:18px;margin:3px;background:#888;box-shadow:0 2px #555555e6;transition:.2s}.form-holder{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}.form-holder form{max-width:434px}form{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch}form .alert{padding:8px 15px;font-size:14px;margin:0 0 25px}.from-group-condition{flex-direction:column;align-self:stretch;justify-content:flex-start;align-items:flex-start;display:flex}.form-group{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;width:100%;flex:1;margin-bottom:20px}.form-group label{flex:1;font-size:12px;line-height:18px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.form-group label.btn-upload{color:#000;font-size:14px;flex:0;flex-basis:auto;margin-top:5px}.form-group label.btn-upload input[type=file]{display:none}.form-group label.btn-upload input[type=text]{margin-bottom:2px;margin-left:10px}.form-group label.btn-upload span.btn{padding-right:20px}.form-group label.btn-upload i{margin-left:3px;margin-right:10px}.form-group .widget{margin-top:10px;align-self:stretch;display:flex;flex-direction:row}.form-group .widget.vertical{flex-direction:column}.form-group .widget.vertical select,.form-group .widget.vertical input{align-self:stretch}.form-group .widget.vertical select:first-child,.form-group .widget.vertical input:first-child{margin-bottom:10px}.form-group .widget .btn{margin-left:10px}.form-group .widget.widget-unit select,.form-group .widget.widget-unit input{flex-grow:0;background:none;box-shadow:none;border:none;border-bottom:1px solid #CCC;border-radius:0;max-width:80px;padding-left:0;color:#000;text-align:center}.form-group .widget.widget-unit span{font-size:12px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-left:5px;color:#aaa}.form-group .widget div{color:#000000b3;font-size:14px}.form-group .widget select,.form-group .widget input,.form-group .widget textarea{outline:none;padding:8px 0 5px 8px;border-radius:2px;border:1px solid rgba(0,0,0,.05);flex:1;background:#aaa;box-shadow:0 2px 1px #bbb,0 4px 2px #ccc inset;color:#222;font-size:14px}.form-group .widget select.input-naked,.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.input-naked,.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.input-naked,.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{color:#aaa;background:none;box-shadow:none;border:none;border-bottom:1px solid #CCC;border-radius:0}.form-group .widget select.input-naked,.form-group .widget input.input-naked,.form-group .widget textarea.input-naked{padding-left:0;color:#444}.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{border:none;background:#fff;border-radius:4px;padding-left:10px;padding-right:10px}.form-group.tab-select{border-bottom:1px solid #BBB;display:flex;flex-direction:row;position:relative;height:48px;padding:48px 0 0;flex:0;flex-basis:auto}.form-group.tab-select .widget{height:49px;margin-top:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;position:absolute;top:0;left:0;border-bottom:2px solid rgb(2,123,255);color:#027bff}.form-group.tab-select .widget select{border:none;background:none;box-shadow:none;padding:10px 35px 10px 10px;margin:0;color:inherit;appearance:none;-moz-appearance:none;-webkit-appearance:none;text-align:left;font-weight:700;cursor:pointer;border-radius:4px 4px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;z-index:2}.form-group.tab-select .widget i{margin-left:10px;margin-right:0}.form-group.tab-select .widget i.triangle{margin-top:-4px;margin-left:0;position:absolute;right:10px}.form-group.form-group-horizontal{margin:10px 0 20px;flex-direction:row;justify-content:flex-start;align-items:center}.form-group.form-group-horizontal .widget{margin:0;display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.actions{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;align-self:stretch;margin:20px 0 0}.actions.actions-intermediate{margin:0}.actions button{margin-left:25px}.actions.actions-left{justify-content:flex-start}.actions.actions-left .btn{margin-left:0;margin-right:25px}.actions.actions-right{justify-content:flex-end}.actions.actions-right .btn{margin-left:10px;margin-right:0}.actions.actions-center{justify-content:center}.actions.actions-center .btn{margin-left:0;margin-right:0}.view-content-list main .main-container .page-content .inner{padding-bottom:10px}.view-content-list main .main-container .content-object-input{margin-bottom:6px}.view-content-list.dragover main .main-container .inner .dropzone{border-radius:4px;background:#0000001a;border:1px dashed rgba(0,0,0,.5)}.view-content-edit main .main-container .bottom-content .page-content{flex:1}.view-content-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px;flex:1}.view-content-edit main .main-container .bottom-content .page-panel.right-panel{flex:2;align-self:stretch;display:flex;flex-direction:column;overflow:hidden;justify-content:flex-start;align-items:center;padding:20px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3{color:#000;padding:10px 10px 10px 0;margin-bottom:20px;font-size:16px;align-self:stretch;margin-left:-8px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 span{border-width:1px;border-style:solid;border-radius:4px;padding:4px 10px;margin-left:5px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel h3 i{font-size:16px}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper{display:flex;flex-direction:column;width:100%;position:relative;padding-top:56.25%;overflow:hidden;border-radius:4px;outline:4px solid rgba(0,0,0,.1)}.view-content-edit main .main-container .bottom-content .page-panel.right-panel .iframe-wrapper iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.view-logs-list main .main-container .bottom-content .page-content .inner{padding-top:8px;padding-bottom:8px}.view-logs-list main .main-container .bottom-content .page-content .logs{flex:1;display:flex;flex-direction:column;align-self:stretch}.view-logs-list main .main-container .bottom-content .page-content .logs pre{flex:1;background:#000000e6;border:1px solid rgba(85,85,85,.5);border-radius:4px;font-family:monospace;color:#f2f2f2;padding:20px;overflow:auto;align-self:stretch}.view-node-player-edit main .main-container .bottom-content .page-content{flex:1}.view-node-player-edit main .main-container .bottom-content .page-content .form-holder{margin:20px 20px 20px 10px}.view-player-group-list main .main-container .players-holder ul.players{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #DDD;border-radius:4px;padding:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .head{display:flex;flex-direction:column;justify-content:center;align-items:center;color:#666;font-size:10px;padding:10px;cursor:default}.view-player-group-list main .main-container .players-holder ul.players li.player-item:hover .infos .title{color:#000}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px;flex:1;max-width:180px;background:#fff;border:1px solid #CCC;border-radius:4px;padding:3px 7px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .title{font-size:13px;color:#555;display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-player-group-list main .main-container .players-holder ul.players li.player-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body{display:block;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#ebebeb;padding:10px;align-self:stretch;flex:1;border-radius:4px;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px;font-size:12px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .body span{opacity:.5;margin-right:7px;font-size:10px}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a{color:#000}.view-playlist-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.view-playlist-list main .main-container .modal-playlist-qrcode h2{text-align:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic{text-align:center;display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .modal-playlist-qrcode .qrcode-pic img{border:4px solid #AAA;border-radius:4px}.view-playlist-list main .main-container .modal-slide h2{font-size:20px}.view-playlist-list main .main-container .modal-slide input[disabled]{color:#555}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select{margin-right:5px}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input{font-size:12px;max-width:50%}.view-playlist-list main .main-container .modal-slide .slide-schedule-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-group input.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group select.datetimepicker,.view-playlist-list main .main-container .modal-slide .slide-schedule-end-group input.datetimepicker{margin-left:5px;padding-left:0}.view-playlist-list main .main-container .bottom-content .page-content{flex:1}.view-playlist-list main .main-container .bottom-content .page-content.with-right-panel{flex:.5}.view-playlist-list main .main-container .bottom-content .page-content .inner{padding:0}.view-playlist-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#222;text-decoration:none;margin:0 0 20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder{margin:20px 20px 20px 10px;flex:1}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder{margin:20px 0 0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder form{max-width:initial}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .form-holder .form-group{flex-grow:0;margin-bottom:5px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder{position:relative}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder .hover-only{display:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#000;padding-bottom:10px;text-decoration:none}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder h4.divide{border-top:1px solid #DDD;margin-top:20px;padding-top:20px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic{margin-top:10px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .qrcode-pic img{border:1px dashed #AAA;padding:5px;border-radius:4px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview{background:#000;border:1px solid rgba(0,0,0,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview iframe{flex:1;align-self:stretch}.view-playlist-list main .main-container .bottom-content .page-content .inner .slides-holder{align-self:stretch;border-right:1px solid #DDD;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-playlist-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-player-group-list main .main-container p{font-size:12px;line-height:18px;display:flex;margin-bottom:5px;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#999}.view-player-group-list main .main-container .bottom-content .page-content{flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner{padding:0}.view-player-group-list main .main-container .bottom-content .page-content .inner h3{font-size:16px;font-weight:500;color:#222;text-decoration:none;margin:0 0 20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder{margin:20px 20px 20px 10px;flex:1}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder{margin:20px 0 0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder form{max-width:initial}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .form-holder .form-group{flex-grow:0;margin-bottom:15px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder{position:relative}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .form-group{flex-grow:0;margin-bottom:0}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder .hover-only{display:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only{display:flex;position:absolute}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview-holder:hover .hover-only:hover{background:#ccc}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4{font-size:14px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;color:#000;padding-bottom:10px;text-decoration:none}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder h4.divide{border-top:1px solid #DDD;margin-top:20px;padding-top:20px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview{background:#000;border:1px solid rgba(0,0,0,.3);border-radius:4px;justify-content:center;align-items:center;align-self:stretch;display:flex;margin:10px 0 20px;height:300px}.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview iframe{flex:1;align-self:stretch}.view-player-group-list main .main-container .bottom-content .page-content .inner .players-holder{align-self:stretch;border-right:1px solid #DDD;margin:20px 10px 20px 20px;padding-right:20px;flex:1.3}.view-player-group-list main .main-container .bottom-content .page-panel.left-panel{flex:.3;max-width:initial;justify-content:center;align-items:center;display:flex}.view-playlist-list main .main-container .page-content .inner h3.divide{margin-top:50px}.view-playlist-list main .main-container .slides-holder ul.slides{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;align-self:stretch;margin:10px 0 0;border:1px dashed #DDD;border-radius:4px;padding:10px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;align-self:stretch;margin:0 0 2px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .title{color:#ccc}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .infos .type i{color:#ccc!important}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item.disabled .body{opacity:0}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort{display:flex;flex-direction:column;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a{color:#666;font-size:10px;padding:10px;cursor:move}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .sort a:hover{color:#027bff}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px;flex:1;max-width:120px;background:#fff;border:1px solid #CCC;border-radius:4px;padding:3px 7px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos:hover .title{color:#000}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .title{display:block;word-break:break-all;font-size:13px;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .infos .type{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;color:#555;font-size:12px;margin-right:5px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin:0 10px;background:#ebebeb;padding:10px;align-self:stretch;flex:1;border-radius:4px;font-size:13px;color:#000}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range{display:flex;flex-direction:column;justify-content:flex-start;align-items:flex-start;flex:1;max-width:315px;overflow-x:auto;white-space:nowrap}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;margin-bottom:8px}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end{display:flex;flex-direction:row;justify-content:flex-start;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .prefix,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .prefix{margin-left:5px;margin-right:5px;font-size:12px;color:#222}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .start .cron-description,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body .range .end .cron-description{display:block;word-break:break-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;margin-left:5px;font-size:10px;opacity:.5}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail{display:flex;flex-direction:row;justify-content:center;align-items:center}.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a{color:#000}.view-plugins-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-settings-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-sysinfo-list .reboot{color:#333}.view-login main .main-container{position:relative}.view-login main .main-container .alert{position:absolute;top:0;left:0;right:0}.view-login main .main-container .login-content{display:flex;flex:1;flex-direction:column;justify-content:center;align-items:center}.view-login main .main-container .login-content .form-holder{width:400px;display:flex;justify-content:center;align-items:center;align-self:stretch;margin-left:auto;margin-right:auto}.view-login main .main-container .login-content .form-holder .card{display:flex;justify-content:center;align-items:center;align-self:stretch;border-radius:6px;padding:50px;color:#fff}.view-login main .main-container .login-content .form-holder .card form{padding:0;flex:1;display:flex;justify-content:center;align-items:center;align-self:stretch}.view-login main .main-container .login-content .form-holder .card form .actions{margin-top:10px}.view-login main .main-container .login-content .form-holder .card form .actions .btn{padding-left:20px;padding-right:20px}.view-auth-user-list main .main-container .bottom-content .page-content .inner{padding:10px}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item.disabled .tile-body{opacity:.3;text-decoration-line:line-through}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-tail .btn{margin-left:10px}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-tail .btn:first-child{margin-left:0}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics{flex:1;flex-direction:row;justify-content:flex-start;align-items:center}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics .widget,.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics .form-group{margin:0}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics label{flex-grow:0}.view-auth-user-list main .main-container .bottom-content .page-content .inner .tile-item .tile-metrics input{margin-left:10px;margin-right:10px;max-width:320px}menu h1.logo a{color:#666}menu:hover h1.logo a{color:transparent}ul.explr-dirview li a.with-thumbnail .img-holder{background:#ddd}ul.explr-dirview li a i{color:#027bff}button.btn-pixel,.btn.btn-pixel{background:#fff;color:#444;box-shadow:4px 0 #ccc,0 4px #11a948,-4px 0 #027bff,0 -4px #ef0e5d}button.btn-pixel:hover,.btn.btn-pixel:hover{box-shadow:6px 0 #ccc,0 6px #11a948,-6px 0 #027bff,0 -6px #ef0e5d}button.btn-naked,.btn.btn-naked{color:#777}button.btn-naked:hover,.btn.btn-naked:hover,button.btn-neutral:hover,.btn.btn-neutral:hover{box-shadow:0 2px 0 1px #666 inset}.tiles .tiles-inner .tile-item{border-top:2px solid transparent;border-right:2px solid transparent;border-bottom:2px solid transparent}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-left:2px solid #E7E7E7;border-top:2px solid #E7E7E7;border-right:2px solid #E7E7E7;border-bottom:2px solid #E7E7E7;background:#fff}.tiles .tiles-inner .tile-item:hover,.tiles .tiles-inner .tile-item.active{border-color:#027bff}.tiles .tiles-inner .tile-item:hover.starred,.tiles .tiles-inner .tile-item.active.starred{border-color:#ffa70a}.tiles .tiles-inner .tile-item:hover.disabled,.tiles .tiles-inner .tile-item.active.disabled{border-color:#bbb}.panes .pane-section .pane-item:nth-child(odd),.panes .pane-section tr:nth-child(odd),.panes tbody .pane-item:nth-child(odd),.panes tbody tr:nth-child(odd){background-color:#fff}.panes .pane-section .pane-item:nth-child(2n),.panes .pane-section tr:nth-child(2n),.panes tbody .pane-item:nth-child(2n),.panes tbody tr:nth-child(2n){background-color:#f7f7f7}.form-group .widget select,.form-group .widget input,.form-group .widget textarea{box-shadow:0 2px 1px #ddd,0 4px 2px #ddd inset;color:#555;background:#eee}.toggle label{box-shadow:0 2px 2px #ccc inset}.toggle label:after{box-shadow:0 2px #cccccce6}.toggle input:checked+label{box-shadow:0 2px 2px #0000004d inset}ul.pills{box-shadow:1px 1px .5px .5px inset #aaa3;background:#eee}ul.pills li a{color:#444;background:#fff}ul.pills li.active a{color:#fff;background:#027bff}.breadcrumb-container ul.breadcrumb{box-shadow:1px 1px .5px .5px inset #aaa3;background:#eee}.breadcrumb-container ul.breadcrumb li a,.breadcrumb-container ul.breadcrumb li span,.breadcrumb-container ul.breadcrumb li{color:#444}.breadcrumb-container ul.breadcrumb li:hover a{color:#fff}.breadcrumb-container ul.breadcrumb li.divider i{color:#bbb}.dropdown ul.dropdown-menu li.danger:hover a{color:#fff}.inner-empty i{color:#ddd;text-shadow:0 -1px #999,0 0px .5px #666}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a{color:#888}.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a:hover,.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .tail a:hover{color:#fff}.form-group .widget select.disabled,.form-group .widget select[disabled],.form-group .widget input.disabled,.form-group .widget input[disabled],.form-group .widget textarea.disabled,.form-group .widget textarea[disabled]{border:none;background:#eee;border-radius:4px;padding-left:10px;padding-right:10px}.modal-explr-picker .explr-tree{background:#f7f7f7} diff --git a/data/www/js/auth/users.js b/data/www/js/auth/users.js index 314d116..c629906 100644 --- a/data/www/js/auth/users.js +++ b/data/www/js/auth/users.js @@ -1,6 +1,8 @@ jQuery(document).ready(function ($) { const main = function () { - + $('.user-token-reveal').each(function() { + updateTokenReveal($(this), false); + }); }; $(document).on('click', '.user-add', function () { @@ -17,5 +19,26 @@ jQuery(document).ready(function ($) { $('#user-edit-id').val(user.id); }); + const updateTokenReveal = function($btn, revealState) { + const $holder = $btn.parents('.user-item:eq(0)'); + const $input = $holder.find('.input-token:eq(0)'); + const $icon = $btn.find('i:eq(0)'); + const isActive = revealState !== undefined ? !revealState : $icon.hasClass('fa-eye-slash'); + + if (isActive) { + $icon.removeClass('fa-eye-slash').addClass('fa-eye'); + $btn.removeClass('btn-neutral').addClass('btn-other'); + $input.val($input.attr('data-private')); + } else { + $icon.removeClass('fa-eye').addClass('fa-eye-slash'); + $btn.removeClass('btn-other').addClass('btn-neutral'); + $input.val($input.attr('data-public')); + } + }; + + $(document).on('click', '.user-token-reveal', function () { + updateTokenReveal($(this)); + }); + main(); }); \ No newline at end of file diff --git a/data/www/scss/pages/_users.scss b/data/www/scss/pages/_users.scss index cd4c9bf..8598795 100644 --- a/data/www/scss/pages/_users.scss +++ b/data/www/scss/pages/_users.scss @@ -15,9 +15,35 @@ } .tile-tail { - a:last-child { + .btn { margin-left: 10px; } + + .btn:first-child { + margin-left: 0; + } + } + + .tile-metrics { + flex: 1; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + .widget, + .form-group { + margin: 0; + } + + label { + flex-grow: 0; + } + + input { + margin-left: 10px; + margin-right: 10px; + max-width: 320px; + } } } } diff --git a/src/model/entity/User.py b/src/model/entity/User.py index 2fdfccd..5aa7bfa 100644 --- a/src/model/entity/User.py +++ b/src/model/entity/User.py @@ -23,7 +23,7 @@ class User: def set_new_apikey(self) -> str: self._apikey = str(uuid.uuid4()) - return self._uuid + return self._apikey @property def id(self) -> Optional[int]: diff --git a/src/service/WebServer.py b/src/service/WebServer.py index 3cef5e1..4f76831 100644 --- a/src/service/WebServer.py +++ b/src/service/WebServer.py @@ -4,7 +4,7 @@ from waitress import serve from functools import wraps from flask import Flask, send_from_directory, redirect, url_for, request, jsonify, make_response, abort -from flask_login import LoginManager, current_user +from flask_login import LoginManager, current_user, login_user from flask_restx import Api from src.manager.UserManager import UserManager @@ -200,6 +200,7 @@ def create_require_api_key_decorator(web_server: WebServer): user = web_server._model_store.user().get_one_by_apikey(apikey) if user: + login_user(user) return user return abort(403, 'Forbidden: You do not have access to this resource.') diff --git a/views/auth/component/table.jinja.html b/views/auth/component/table.jinja.html index 7ac2c62..a9cca46 100644 --- a/views/auth/component/table.jinja.html +++ b/views/auth/component/table.jinja.html @@ -1,12 +1,25 @@
{% for user in users %} -
{{ truncate(user.username, 100, '...') }}
+
+
+
+ + +
+
+
+ From 5fba1a430086c892ddd5435f818d41e39d0197d8 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 17:42:06 +0200 Subject: [PATCH 7/9] conditionnal token if plugin enabled --- README.md | 2 +- src/controller/AuthController.py | 1 + views/auth/component/table.jinja.html | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3754a8f..416ba29 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It is a temporary live demo, all data will be deleted after 30 minutes (~30secs - Playlist management - Authentication management - Plays content from flashdrive in offline mode -- Plugin system to extend capabilities +- Core API & Plugin system to extend capabilities - [Multi Languages](https://github.com/jr-k/obscreen/tree/master/lang) - Cast pictures and iframes to Chromecast - No costly monthly pricing plan per screen or whatever, no cloud, no telemetry diff --git a/src/controller/AuthController.py b/src/controller/AuthController.py index 548a9ce..799a965 100644 --- a/src/controller/AuthController.py +++ b/src/controller/AuthController.py @@ -69,6 +69,7 @@ class AuthController(ObController): 'auth/list.jinja.html', error=request.args.get('error', None), users=self._model_store.user().get_users(exclude=User.DEFAULT_USER if demo else None), + plugin_core_api_enabled=self._model_store.variable().map().get('plugin_core_api_enabled').as_bool() ) def auth_user_add(self): diff --git a/views/auth/component/table.jinja.html b/views/auth/component/table.jinja.html index a9cca46..4ae73cc 100644 --- a/views/auth/component/table.jinja.html +++ b/views/auth/component/table.jinja.html @@ -6,6 +6,7 @@
{{ truncate(user.username, 100, '...') }}
+ {% if plugin_core_api_enabled %}
@@ -16,10 +17,13 @@
+ {% endif %}
+ {% if plugin_core_api_enabled %} + {% endif %} From d88db002f1a7cbc8ebe294561c7c6593b3a4b2a1 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 18:02:42 +0200 Subject: [PATCH 8/9] fully working api --- data/www/js/plugins.js | 2 +- plugins/system/CoreApi/CoreApi.py | 3 +++ plugins/system/CoreApi/lang/en.json | 3 ++- plugins/system/CoreApi/lang/es.json | 3 ++- plugins/system/CoreApi/lang/fr.json | 3 ++- plugins/system/CoreApi/lang/it.json | 3 ++- plugins/system/CoreUpdater/CoreUpdater.py | 3 +++ plugins/user/Dashboard/Dashboard.py | 3 +++ src/interface/ObPlugin.py | 9 ++++++++- src/service/PluginStore.py | 11 ++++++----- src/util/utils.py | 1 - views/configuration/plugins/modal/edit.jinja.html | 4 +--- 12 files changed, 33 insertions(+), 15 deletions(-) diff --git a/data/www/js/plugins.js b/data/www/js/plugins.js index c54d4e3..2c290ec 100644 --- a/data/www/js/plugins.js +++ b/data/www/js/plugins.js @@ -22,7 +22,7 @@ jQuery(document).ready(function ($) { $('.modal-variable-edit input:visible:eq(0)').focus().select(); $('#variable-edit-name').val(variable.name); $('#variable-edit-description').html(variable.description); - $('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === ''); + $('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === '' || variable.description_edition === null); $('#variable-edit-value').val(variable.value); $('#variable-edit-id').val(variable.id); }); diff --git a/plugins/system/CoreApi/CoreApi.py b/plugins/system/CoreApi/CoreApi.py index a5558f7..b1a654f 100644 --- a/plugins/system/CoreApi/CoreApi.py +++ b/plugins/system/CoreApi/CoreApi.py @@ -20,6 +20,9 @@ class CoreApi(ObPlugin): def use_description(self): return self.translate('plugin_description') + def use_help_on_activation(self): + return self.translate('plugin_help_on_activation') + def use_variables(self) -> List[Variable]: return [] diff --git a/plugins/system/CoreApi/lang/en.json b/plugins/system/CoreApi/lang/en.json index 231fe24..01d9c66 100644 --- a/plugins/system/CoreApi/lang/en.json +++ b/plugins/system/CoreApi/lang/en.json @@ -1,4 +1,5 @@ { "plugin_title": "Core API", - "plugin_description": "Adds api feature wrapping core features" + "plugin_description": "Adds api feature wrapping core features", + "plugin_help_on_activation": "Documentation will be available on the /api page" } diff --git a/plugins/system/CoreApi/lang/es.json b/plugins/system/CoreApi/lang/es.json index 91c3d59..b05e2f9 100644 --- a/plugins/system/CoreApi/lang/es.json +++ b/plugins/system/CoreApi/lang/es.json @@ -1,4 +1,5 @@ { "plugin_title": "Core API", - "plugin_description": "Agrega características de API que envuelven las características principales" + "plugin_description": "Agrega características de API que envuelven las características principales", + "plugin_help_on_activation": "La documentación estará disponible en la página /api" } \ No newline at end of file diff --git a/plugins/system/CoreApi/lang/fr.json b/plugins/system/CoreApi/lang/fr.json index 83b67e6..382c842 100644 --- a/plugins/system/CoreApi/lang/fr.json +++ b/plugins/system/CoreApi/lang/fr.json @@ -1,4 +1,5 @@ { "plugin_title": "Core API", - "plugin_description": "Ajoute des fonctionnalités d'API englobant les fonctionnalités principales" + "plugin_description": "Ajoute des fonctionnalités d'API englobant les fonctionnalités principales", + "plugin_help_on_activation": "La documentation sera disponible sur la page /api" } \ No newline at end of file diff --git a/plugins/system/CoreApi/lang/it.json b/plugins/system/CoreApi/lang/it.json index de1a4a3..8e3de5f 100644 --- a/plugins/system/CoreApi/lang/it.json +++ b/plugins/system/CoreApi/lang/it.json @@ -1,4 +1,5 @@ { "plugin_title": "Core API", - "plugin_description": "Aggiunge funzionalità API che racchiudono le funzionalità di base" + "plugin_description": "Aggiunge funzionalità API che racchiudono le funzionalità di base", + "plugin_help_on_activation": "La documentazione sarà disponibile nella pagina /api" } \ No newline at end of file diff --git a/plugins/system/CoreUpdater/CoreUpdater.py b/plugins/system/CoreUpdater/CoreUpdater.py index 787be47..52503b7 100644 --- a/plugins/system/CoreUpdater/CoreUpdater.py +++ b/plugins/system/CoreUpdater/CoreUpdater.py @@ -22,6 +22,9 @@ class GitUpdater(ObPlugin): def use_description(self): return self.translate('plugin_description') + def use_help_on_activation(self): + return None + def use_variables(self) -> List[Variable]: return [] diff --git a/plugins/user/Dashboard/Dashboard.py b/plugins/user/Dashboard/Dashboard.py index b85dcc7..1f0b9d7 100644 --- a/plugins/user/Dashboard/Dashboard.py +++ b/plugins/user/Dashboard/Dashboard.py @@ -20,6 +20,9 @@ class Dashboard(ObPlugin): def use_description(self): return self.translate('plugin_description') + def use_help_on_activation(self): + return None + def use_variables(self) -> List[Variable]: return [] diff --git a/src/interface/ObPlugin.py b/src/interface/ObPlugin.py index 54bca9f..374b1c7 100644 --- a/src/interface/ObPlugin.py +++ b/src/interface/ObPlugin.py @@ -38,6 +38,10 @@ class ObPlugin(abc.ABC): def use_description(self) -> str: pass + @abc.abstractmethod + def use_help_on_activation(self) -> Optional[str]: + pass + @abc.abstractmethod def use_variables(self) -> List[Variable]: pass @@ -86,7 +90,10 @@ class ObPlugin(abc.ABC): def add_functional_hook_registration(self, hook: HookType, priority: int = 0, function=None) -> FunctionalHookRegistration: return FunctionalHookRegistration(plugin=self, hook=hook, priority=priority, function=function) - def translate(self, token, resolve=False) -> Union[Dict, str]: + def translate(self, token, resolve=True) -> Union[Dict, str]: + if not token: + token = '' + token = token if token.startswith(self.use_id()) else "{}_{}".format(self.use_id(), token) return self._model_store.lang().translate(token) if resolve else token diff --git a/src/service/PluginStore.py b/src/service/PluginStore.py index e435c37..36aea5f 100644 --- a/src/service/PluginStore.py +++ b/src/service/PluginStore.py @@ -120,6 +120,10 @@ class PluginStore: self._hooks[hook_type] = sorted(self._hooks[hook_type], key=lambda hook_reg: hook_reg.priority, reverse=True) def setup_plugin(self, plugin: ObPlugin) -> None: + # LANGS + self._model_store.lang().load(directory=plugin.get_directory(), prefix=plugin.use_id()) + self._model_store.variable().reload() + # VARIABLES variables = plugin.use_variables() + [ plugin.add_variable( @@ -127,7 +131,8 @@ class PluginStore: value=False, type=VariableType.BOOL, editable=True, - description=self._model_store.lang().translate("common_enable_plugin") + description=self._model_store.lang().translate("common_enable_plugin"), + description_edition=plugin.use_help_on_activation() ) ] @@ -135,10 +140,6 @@ class PluginStore: if variable.name in self._dead_variables_candidates: del self._dead_variables_candidates[variable.name] - # LANGS - self._model_store.lang().load(directory=plugin.get_directory(), prefix=plugin.use_id()) - self._model_store.variable().reload() - if not self.is_plugin_enabled(plugin): return diff --git a/src/util/utils.py b/src/util/utils.py index c4fce2d..b937700 100644 --- a/src/util/utils.py +++ b/src/util/utils.py @@ -60,7 +60,6 @@ def camel_to_snake(camel: str) -> str: def str_datetime_to_cron(datetime_str: str) -> str: - print(datetime_str) datetime_obj = datetime.strptime(datetime_str, '%Y-%m-%d %H:%M') return "{} {} {} {} * {}".format( datetime_obj.minute, diff --git a/views/configuration/plugins/modal/edit.jinja.html b/views/configuration/plugins/modal/edit.jinja.html index 5610952..c5de667 100644 --- a/views/configuration/plugins/modal/edit.jinja.html +++ b/views/configuration/plugins/modal/edit.jinja.html @@ -13,9 +13,7 @@
-
- -
+
From f717c6a82fa3d54183202da220eca68c9edecf38 Mon Sep 17 00:00:00 2001 From: jr-k Date: Sun, 4 Aug 2024 18:02:53 +0200 Subject: [PATCH 9/9] bump --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 5859406..276cbf9 100755 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.2.3 +2.3.0