From 6afca61dd953d01bf9c5fcb1c936f224aae01b4d Mon Sep 17 00:00:00 2001 From: Tiberiu Chibici Date: Thu, 23 Apr 2020 00:47:27 +0300 Subject: [PATCH] Began refactoring javascript code --- .../management/video_provider_manager.py | 34 ++- .../providers/dummy_video_provider.py | 47 ++++ app/YtManagerApp/providers/video_provider.py | 16 +- .../providers/ytapi_video_provider.py | 6 +- app/YtManagerApp/services.py | 7 +- .../static/YtManagerApp/css/style.css | 154 +++++++++++-- .../static/YtManagerApp/css/style.css.map | 4 +- .../static/YtManagerApp/css/style.scss | 78 +++---- .../img/video_providers/Dummy.png | Bin 0 -> 12341 bytes .../img/video_providers/YtAPI.png | Bin 0 -> 20462 bytes .../static/YtManagerApp/js/common.js | 28 +++ .../YtManagerApp/js/components/AjaxModal.js | 167 ++++++++++++++ .../YtManagerApp/js/components/JobPanel.js | 213 ++++++++++++++++++ .../templates/YtManagerApp/index.html | 12 +- .../templates/YtManagerApp/js/common.js | 169 -------------- .../templates/YtManagerApp/js/index.js | 114 ++-------- .../YtManagerApp/master_default.html | 100 ++++---- .../YtManagerApp/settings/providers.html | 36 +++ app/YtManagerApp/urls.py | 4 +- app/YtManagerApp/views/settings/__init__.py | 0 .../views/settings/providers_view.py | 31 +++ .../views/{ => settings}/settings.py | 0 22 files changed, 832 insertions(+), 388 deletions(-) create mode 100644 app/YtManagerApp/providers/dummy_video_provider.py create mode 100644 app/YtManagerApp/static/YtManagerApp/img/video_providers/Dummy.png create mode 100644 app/YtManagerApp/static/YtManagerApp/img/video_providers/YtAPI.png create mode 100644 app/YtManagerApp/static/YtManagerApp/js/common.js create mode 100644 app/YtManagerApp/static/YtManagerApp/js/components/AjaxModal.js create mode 100644 app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js create mode 100644 app/YtManagerApp/templates/YtManagerApp/settings/providers.html create mode 100644 app/YtManagerApp/views/settings/__init__.py create mode 100644 app/YtManagerApp/views/settings/providers_view.py rename app/YtManagerApp/views/{ => settings}/settings.py (100%) diff --git a/app/YtManagerApp/management/video_provider_manager.py b/app/YtManagerApp/management/video_provider_manager.py index 6db28ae..e411105 100644 --- a/app/YtManagerApp/management/video_provider_manager.py +++ b/app/YtManagerApp/management/video_provider_manager.py @@ -1,12 +1,15 @@ import logging -from typing import List, Dict, Union +from typing import List, Dict, Union, Iterable from YtManagerApp.models import VideoProviderConfig, Video, Subscription from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError import json +from collections import namedtuple log = logging.getLogger("VideoProviderManager") +VideoProviderInfo = namedtuple('VideoProviderInfo', ['id', 'name', 'is_configured', 'description']) + class VideoProviderManager(object): def __init__(self, registered_providers: List[VideoProvider]): @@ -23,18 +26,18 @@ class VideoProviderManager(object): :param provider: Video provider """ # avoid duplicates - if provider.name in self._registered_providers: - log.error(f"Duplicate video provider {provider.name}") + if provider.id in self._registered_providers: + log.error(f"Duplicate video provider {provider.id}") return # register - self._registered_providers[provider.name] = provider - log.info(f"Registered video provider {provider.name}") + self._registered_providers[provider.id] = provider + log.info(f"Registered video provider {provider.id}") # load configuration (if any) - if provider.name in self._pending_configs: - self._configure(provider, self._pending_configs[provider.name]) - del self._pending_configs[provider.name] + if provider.id in self._pending_configs: + self._configure(provider, self._pending_configs[provider.id]) + del self._pending_configs[provider.id] def _load(self) -> None: # Loads configuration from database @@ -53,8 +56,8 @@ class VideoProviderManager(object): def _configure(self, provider, config): settings = json.loads(config.settings) provider.configure(settings) - log.info(f"Configured video provider {provider.name}") - self._configured_providers[provider.name] = provider + log.info(f"Configured video provider {provider.id}") + self._configured_providers[provider.id] = provider def get(self, item: Union[str, Subscription, Video]): """ @@ -100,3 +103,14 @@ class VideoProviderManager(object): pass raise InvalidURLError("The given URL is not valid for any of the supported sites!") + + def get_available_providers(self) -> Iterable[VideoProviderInfo]: + """ + Gets a list of available providers and some basic information about them. + :return: List of dictionary entries + """ + for key, provider in self._registered_providers.items(): + yield VideoProviderInfo(id=key, + name=provider.name, + description=provider.description, + is_configured=(key in self._configured_providers)) diff --git a/app/YtManagerApp/providers/dummy_video_provider.py b/app/YtManagerApp/providers/dummy_video_provider.py new file mode 100644 index 0000000..8fc1c12 --- /dev/null +++ b/app/YtManagerApp/providers/dummy_video_provider.py @@ -0,0 +1,47 @@ +from typing import Dict, Optional, Any, Iterable, List + +from django import forms +from external.pytaw.pytaw import youtube as yt +from external.pytaw.pytaw.utils import iterate_chunks + +from YtManagerApp.models import Subscription, Video +from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError, ProviderValidationError + + +class DummyVideoProvider(VideoProvider): + id = "Dummy" + name = "Dummy Videos" + description = "Won't really do anything, it's here just for testing." + settings = { + "api_key": forms.CharField(label="Dummy API Key"), + "number_of_something": forms.IntegerField(label="Number of stuff") + } + + def configure(self, configuration: Dict[str, Any]) -> None: + print(configuration) + + def validate_configuration(self, configuration: Dict[str, Any]): + print("Validating...") + if configuration["number_of_something"] >= 10: + raise ProviderValidationError( + field_messages={'number_of_something': "Number too large, try something smaller!"}) + pass + + def get_subscription_url(self, subscription: Subscription): + return f"https://dummy/playlist/{subscription.playlist_id}" + + def validate_subscription_url(self, url: str) -> None: + if not url.startswith('https://dummy/'): + raise InvalidURLError("URL not valid") + + def fetch_subscription(self, url: str) -> Subscription: + raise ValueError('No such subscription (note: dummy plugin, nothing will work)!') + + def get_video_url(self, video: Video) -> str: + return f"https://dummy/video/{video.video_id}" + + def fetch_videos(self, subscription: Subscription) -> Iterable[Video]: + return [] + + def update_videos(self, videos: List[Video], update_metadata=False, update_statistics=False) -> None: + pass diff --git a/app/YtManagerApp/providers/video_provider.py b/app/YtManagerApp/providers/video_provider.py index 63453aa..f377fe4 100644 --- a/app/YtManagerApp/providers/video_provider.py +++ b/app/YtManagerApp/providers/video_provider.py @@ -6,7 +6,7 @@ from django.forms import Field from YtManagerApp.models import Subscription, Video -class ConfigurationValidationError(ValueError): +class ProviderValidationError(ValueError): """ Exception type thrown when validating configurations. """ @@ -27,7 +27,21 @@ class InvalidURLError(ValueError): class VideoProvider(ABC): + """ + Identifier + """ + id: str = "" + """ + Display name, shown to users + """ name: str = "" + """ + Description, shown to users + """ + description: str = "" + """ + Dictionary containing fields necessary for configuring + """ settings: Dict[str, Field] = {} @abstractmethod diff --git a/app/YtManagerApp/providers/ytapi_video_provider.py b/app/YtManagerApp/providers/ytapi_video_provider.py index 98bb752..538b394 100644 --- a/app/YtManagerApp/providers/ytapi_video_provider.py +++ b/app/YtManagerApp/providers/ytapi_video_provider.py @@ -9,7 +9,9 @@ from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError class YouTubeApiVideoProvider(VideoProvider): - name = "YtAPI" + id = "YtAPI" + name = "YouTube API" + description = "Allows communication with YouTube using the YouTube API." settings = { "api_key": forms.CharField(label="YouTube API Key:") } @@ -44,7 +46,7 @@ class YouTubeApiVideoProvider(VideoProvider): def fetch_subscription(self, url: str) -> Subscription: sub = Subscription() - sub.provider_id = self.name + sub.provider_id = self.id self.validate_subscription_url(url) url_parsed = self.__api.parse_url(url) diff --git a/app/YtManagerApp/services.py b/app/YtManagerApp/services.py index 7393580..6cb5221 100644 --- a/app/YtManagerApp/services.py +++ b/app/YtManagerApp/services.py @@ -12,7 +12,9 @@ from YtManagerApp.scheduler.scheduler import YtsmScheduler class VideoProviders(containers.DeclarativeContainer): from YtManagerApp.providers.ytapi_video_provider import YouTubeApiVideoProvider + from YtManagerApp.providers.dummy_video_provider import DummyVideoProvider ytApiProvider = providers.Factory(YouTubeApiVideoProvider) + dummyProvider = providers.Factory(DummyVideoProvider) class Services(containers.DeclarativeContainer): @@ -21,6 +23,9 @@ class Services(containers.DeclarativeContainer): scheduler = providers.Singleton(YtsmScheduler, appConfig) youtubeDLManager = providers.Singleton(YoutubeDlManager) videoManager = providers.Singleton(VideoManager) - videoProviderManager = providers.Singleton(VideoProviderManager, [VideoProviders.ytApiProvider()]) + videoProviderManager = providers.Singleton(VideoProviderManager, [ + VideoProviders.ytApiProvider(), + VideoProviders.dummyProvider(), + ]) subscriptionManager = providers.Singleton(SubscriptionManager) downloadManager = providers.Singleton(DownloadManager) diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.css b/app/YtManagerApp/static/YtManagerApp/css/style.css index 58fcaa6..a473add 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.css +++ b/app/YtManagerApp/static/YtManagerApp/css/style.css @@ -1,18 +1,42 @@ -#main_body { - margin-bottom: 4rem; - margin-top: 0; } +:root { + --blue: #007bff; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #dc3545; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #28a745; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #f8f9fa; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } -#main_footer { - position: fixed; - left: 0; - right: 0; - bottom: 0; - height: 2rem; - line-height: 2rem; - padding: 0 1rem; - display: flex; - align-content: center; - font-size: 10pt; } +[data-theme="dark"] { + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #343a40; + --dark: #f8f9fa; } /* Loading animation */ .loading-dual-ring { @@ -26,8 +50,8 @@ height: 46px; margin: 1px; border-radius: 50%; - border: 5px solid #007bff; - border-color: #007bff transparent #007bff transparent; + border: 5px solid var(--primary); + border-color: var(--primary) transparent var(--primary) transparent; animation: loading-dual-ring 1.2s linear infinite; } .loading-dual-ring-small { @@ -41,8 +65,8 @@ height: 23px; margin: 1px; border-radius: 50%; - border: 2.5px solid #007bff; - border-color: #007bff transparent #007bff transparent; + border: 2.5px solid var(--primary); + border-color: var(--primary) transparent var(--primary) transparent; animation: loading-dual-ring 1.2s linear infinite; } @keyframes loading-dual-ring { @@ -57,6 +81,84 @@ margin-top: -32px; margin-left: -32px; } +#hamburger-button { + margin-right: 0.5rem; } + #hamburger-button span { + position: relative; + width: 1.75rem; + height: 0.25rem; + margin-bottom: 0.25rem; + display: block; + background: #cdcdcd; + border-radius: 0.25rem; + transform-origin: 50% 50%; + transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease; } + #hamburger-button span:nth-child(1) { + margin-top: 0.25rem; } + #hamburger-button span:nth-child(2) { + transform-origin: 20% 50%; } + #hamburger-button.hamburger-show span { + opacity: 1; + background: #232323; } + #hamburger-button.hamburger-show span:nth-child(1) { + transform: rotate(-45deg) translate(-0.1875rem, 0.0625rem) scale(0.625, 1); } + #hamburger-button.hamburger-show span:nth-child(2) { + opacity: 0; + transform: rotate(0deg) scale(0, 1); } + #hamburger-button.hamburger-show span:nth-child(3) { + transform: rotate(45deg) translate(-0.1875rem, -0.0625rem) scale(0.625, 1); } + +#hamburger { + position: fixed; + top: 0; + left: -100%; + z-index: 99; + padding-top: 5rem; + width: 20rem; + height: 100%; + display: flex; + background: white; + flex-direction: column; + transition: left 0.5s ease; } + #hamburger.hamburger-show { + left: 0; } + @media (max-width: 768px) { + #hamburger { + width: 100%; } } + +#hamburger-footer { + width: 100%; + flex: 0; } + #hamburger-footer a { + margin-left: 0.5rem; + margin-right: 0.5rem; } + +#hamburger-content { + overflow-y: auto; + flex: 1; } + +#main_navbar { + position: fixed; + top: 0; + width: 100%; + z-index: 100; } + +#main_body { + margin-bottom: 4rem; + margin-top: 4rem; } + +#main_footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: 2rem; + line-height: 2rem; + padding: 0 1rem; + display: flex; + align-content: center; + font-size: 10pt; } + .black-overlay { position: fixed; /* Sit on top of the page content */ @@ -168,4 +270,20 @@ img.muted { opacity: .5; } +.provider-wrapper { + padding-left: 5px; + padding-right: 5px; } + .provider-wrapper button { + width: 100%; } + +.provider-logo { + display: inline-block; + width: 8rem; + height: 4rem; + object-fit: contain; + margin: .25em auto; + font-size: 3rem; + text-align: center; + line-height: 4rem; } + /*# sourceMappingURL=style.css.map */ diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.css.map b/app/YtManagerApp/static/YtManagerApp/css/style.css.map index 1857bfa..6d30ccb 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.css.map +++ b/app/YtManagerApp/static/YtManagerApp/css/style.css.map @@ -1,7 +1,7 @@ { "version": 3, -"mappings": "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", -"sources": ["style.scss"], +"mappings": "AAAA,KAAM;EAEF,MAAM,CAAC,QAAQ;EACf,QAAQ,CAAC,QAAQ;EACjB,QAAQ,CAAC,QAAQ;EACjB,MAAM,CAAC,QAAQ;EACf,KAAK,CAAC,QAAQ;EACd,QAAQ,CAAC,QAAQ;EACjB,QAAQ,CAAC,QAAQ;EACjB,OAAO,CAAC,QAAQ;EAChB,MAAM,CAAC,QAAQ;EACf,MAAM,CAAC,QAAQ;EACf,OAAO,CAAC,KAAK;EACb,MAAM,CAAC,QAAQ;EACf,WAAW,CAAC,QAAQ;EAGpB,SAAS,CAAC,QAAQ;EAClB,WAAW,CAAC,QAAQ;EACpB,SAAS,CAAC,QAAQ;EAClB,MAAM,CAAC,QAAQ;EACf,SAAS,CAAC,QAAQ;EAClB,QAAQ,CAAC,QAAQ;EACjB,OAAO,CAAC,QAAQ;EAChB,MAAM,CAAC,QAAQ;EAGf,eAAe,CAAC,EAAE;EAClB,eAAe,CAAC,MAAM;EACtB,eAAe,CAAC,MAAM;EACtB,eAAe,CAAC,MAAM;EACtB,eAAe,CAAC,OAAO;EACvB,wBAAwB,CAAC,sLAAsL;EAC/M,uBAAuB,CAAC,qFAAqF;;AAGjH,mBAAoB;EAChB,SAAS,CAAC,QAAQ;EAClB,WAAW,CAAC,QAAQ;EACpB,SAAS,CAAC,QAAQ;EAClB,MAAM,CAAC,QAAQ;EACf,SAAS,CAAC,QAAQ;EAClB,QAAQ,CAAC,QAAQ;EACjB,OAAO,CAAC,QAAQ;EAChB,MAAM,CAAC,QAAQ;;ACzBnB,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,wBAAmC;IAC3C,YAAY,EAAE,qDAAqD;IACnE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,0BAAmC;IAC3C,YAAY,EAAE,qDAAqD;IACnE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;ACxCtB,iBAAkB;EACd,YAAY,EAAE,MAAM;EAEpB,sBAAK;IACD,QAAQ,EAAE,QAAQ;IAElB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,OAAO;IACf,aAAa,EAAE,OAAO;IAEtB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,OAAO;IACnB,aAAa,EAAE,OAAO;IAEtB,gBAAgB,EAAE,OAAO;IAEzB,UAAU,EAAE,qHAEkB;IAE9B,mCAAe;MACX,UAAU,EAAE,OAAO;IAEvB,mCAAe;MACX,gBAAgB,EAAE,OAAO;EAK7B,qCAAK;IACD,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,OAAO;IAEnB,kDAAe;MACX,SAAS,EAAE,+DAA8D;IAE7E,kDAAe;MACX,OAAO,EAAE,CAAC;MACV,SAAS,EAAE,wBAAwB;IAEvC,kDAAe;MACX,SAAS,EAAE,+DAA+D;;AAM1F,UAAW;EACP,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,KAAK;EACX,OAAO,EAAE,EAAE;EACX,WAAW,EAAE,IAAI;EAEjB,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,IAAI;EAEZ,OAAO,EAAE,IAAI;EACb,UAAU,EAAE,KAAK;EACjB,cAAc,EAAE,MAAM;EAEtB,UAAU,EAAE,cAAc;EAE1B,yBAAiB;IACb,IAAI,EAAE,CAAC;EAGX,yBAAmC;IApBvC,UAAW;MAqBH,KAAK,EAAE,IAAI;;AAInB,iBAAkB;EACd,KAAK,EAAE,IAAI;EACX,IAAI,EAAE,CAAC;EAEP,mBAAE;IACE,WAAW,EAAE,MAAM;IACnB,YAAY,EAAE,MAAM;;AAI5B,kBAAmB;EACf,UAAU,EAAE,IAAI;EAChB,IAAI,EAAE,CAAC;;AC/EX,YAAa;EACT,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,GAAG;;AAGhB,UAAW;EACP,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,IAAI;;AAGpB,YAAa;EACT,QAAQ,EAAE,KAAK;EACf,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,MAAM;EACf,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,MAAM;EACrB,SAAS,EAAE,IAAI;;AAGnB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,KAAK;EAEpB,kCAAQ;IACJ,gBAAgB,EAAE,OAAO;AAGjC,oBAAM;EACF,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,IAAI;EAEhB,+BAAW;IACP,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,CAAC;EAEd,+BAAW;IACP,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,KAAK;EAExB,gCAAY;IACR,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,KAAK;IACpB,WAAW,EAAE,MAAM;IAEnB,uCAAO;MACH,SAAS,EAAE,GAAG;EAGtB,iCAAa;IACT,OAAO,EAAE,YAAY;EAGzB,+BAAW;IACP,YAAY,EAAE,QAAQ;IACtB,UAAU,EAAE,QAAQ;IACpB,qCAAQ;MACJ,eAAe,EAAE,IAAI;EAO7B,8BAAU;IACN,KAAK,EAAE,KAAK;;AAMxB,aAAc;EACV,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,KAAK;EAEZ,0BAAa;IACT,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,MAAM;IAEnB,gBAAgB,EAAE,IAAI;IACtB,UAAU,EAAE,kCAA6B;IACzC,aAAa,EAAE,WAAW;IAE1B,KAAK,EAAE,KAAK;IACZ,UAAU,EAAE,MAAM;IAClB,WAAW,EAAE,uCAAuC;IACpD,WAAW,EAAE,GAAG;IAChB,SAAS,EAAE,GAAG;IACd,cAAc,EAAE,SAAS;IAEzB,0CAAkB;MACd,gBAAgB,EAtHb,OAAO;IAwHd,iDAAyB;MACrB,gBAAgB,EAAE,OAAO;IAE7B,8CAAsB;MAClB,gBAAgB,EAAE,IAAI;;AAMlC,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC;;AAIjB,YAAa;EACT,OAAO,EAAE,YAAY;EACrB,aAAa,EAAE,MAAM;;AAGzB,YAAa;EACT,MAAM,EAAE,OAAO;EACf,iBAAK;IACD,OAAO,EAAE,cAAc;IACvB,SAAS,EAAE,IAAI;;AAIvB,iBAAkB;EACd,YAAY,EAAE,OAAO;;AAGzB,cAAe;EACX,SAAS,EAAE,KAAK;EAEhB,+BAAiB;IACb,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,MAAM;;AAI3B,SAAU;EACN,OAAO,EAAE,EAAE;;AAGf,iBAAkB;EACd,YAAY,EAAE,GAAG;EACjB,aAAa,EAAE,GAAG;EAClB,wBAAO;IACH,KAAK,EAAE,IAAI;;AAInB,cAAe;EACX,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,OAAO;EACnB,MAAM,EAAE,UAAU;EAElB,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,IAAI", +"sources": ["lib/_theme.scss","lib/_loading.scss","lib/_hamburger.scss","style.scss"], "names": [], "file": "style.css" } \ No newline at end of file diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.scss b/app/YtManagerApp/static/YtManagerApp/css/style.scss index 7d020ff..430b197 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.scss +++ b/app/YtManagerApp/static/YtManagerApp/css/style.scss @@ -1,8 +1,20 @@ +@import "lib/theme"; +@import "lib/loading"; +@import "lib/hamburger"; + $accent-color: #007bff; +$success-color: #28a745; + +#main_navbar { + position: fixed; + top: 0; + width: 100%; + z-index: 100; +} #main_body { margin-bottom: 4rem; - margin-top: 0; + margin-top: 4rem; } #main_footer { @@ -18,50 +30,6 @@ $accent-color: #007bff; font-size: 10pt; } -@mixin loading-dual-ring($scale : 1) { - display: inline-block; - width: $scale * 64px; - height: $scale * 64px; - - &:after { - content: " "; - display: block; - width: $scale * 46px; - height: $scale * 46px; - margin: 1px; - border-radius: 50%; - border: ($scale * 5px) solid $accent-color; - border-color: $accent-color transparent $accent-color transparent; - animation: loading-dual-ring 1.2s linear infinite; - } -} - -/* Loading animation */ -.loading-dual-ring { - @include loading-dual-ring(1.0); -} - -.loading-dual-ring-small { - @include loading-dual-ring(0.5); -} - -@keyframes loading-dual-ring { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.loading-dual-ring-center-screen { - position: fixed; - top: 50%; - left: 50%; - margin-top: -32px; - margin-left: -32px; -} - .black-overlay { position: fixed; /* Sit on top of the page content */ display: none; /* Hidden by default */ @@ -212,4 +180,24 @@ $accent-color: #007bff; img.muted { opacity: .5; +} + +.provider-wrapper { + padding-left: 5px; + padding-right: 5px; + button { + width: 100%; + } +} + +.provider-logo { + display: inline-block; + width: 8rem; + height: 4rem; + object-fit: contain; + margin: .25em auto; + + font-size: 3rem; + text-align: center; + line-height: 4rem; } \ No newline at end of file diff --git a/app/YtManagerApp/static/YtManagerApp/img/video_providers/Dummy.png b/app/YtManagerApp/static/YtManagerApp/img/video_providers/Dummy.png new file mode 100644 index 0000000000000000000000000000000000000000..01abd6c95fb9008c055ec999212b2f50f22eaa94 GIT binary patch literal 12341 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 eXJd8eB*1(b0wJM>CtMF2NlF!9s9%`|do?`|k7o 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 error occurred while displaying the dialog!'); + } + + _submit(e) { + let pThis = this; + let url = this.form.attr('action'); + let ajax_settings = { + url: url, + }; + + if (this.form.attr('enctype') === 'multipart/form-data') { + ajax_settings.data = new FormData(this.form[0]); + ajax_settings.contentType = false; + ajax_settings.processData = false; + ajax_settings.cache = false; + } + else { + ajax_settings.data = this.form.serialize(); + } + + $.post(ajax_settings) + .done(function(result) { + pThis._submitDone(result); + }) + .fail(function() { + pThis._submitFailed(); + }) + .always(function() { + pThis.modalLoadingRing.fadeOut(100); + pThis.wrapper.find(":input").prop("disabled", false); + }); + + this.modalLoadingRing.fadeIn(200); + this.wrapper.find(":input").prop("disabled", true); + + e.preventDefault(); + } + + _submitDone(result) { + // Clear old errors first + this.form.find('.modal-field-error').remove(); + + if (!result.hasOwnProperty('success')) { + this._submitInvalidResponse(); + return; + } + + if (result.success) { + this._hideModal(); + if (this.submitCallback != null) + this.submitCallback(); + } + else { + if (!result.hasOwnProperty('errors')) { + this._submitInvalidResponse(); + return; + } + + for (let field in result.errors) + { + let errorsArray = result.errors[field]; + let errorsConcat = "
    "; + + for(let error of errorsArray) { + errorsConcat += `
  • ${error.message}
  • `; + } + errorsConcat += '
'; + + if (field === '__all__') + this.form.find('.modal-body').append(errorsConcat); + else + this.form.find(`[name='${field}']`).after(errorsConcat); + } + + let errorsHtml = ''; + + let err = this.modal.find('#__modal_error'); + if (err.length) { + err.html('An error occurred'); + } + else { + this.modal.find('.modal-body').append(errorsHtml) + } + } + } + + _submitFailed() { + // Clear old errors first + this.form.find('.modal-field-error').remove(); + this.form.find('.modal-body') + .append(``); + } + + _submitInvalidResponse() { + // Clear old errors first + this.form.find('.modal-field-error').remove(); + this.form.find('.modal-body') + .append(``); + } + + loadAndShow() + { + let pThis = this; + this._showLoading(); + + $.get(this.url) + .done(function (result) { + pThis._load(result); + pThis._showModal(); + }) + .fail(function () { + pThis._loadFailed(); + }) + .always(function() { + pThis._hideLoading(); + }); + } +} \ No newline at end of file diff --git a/app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js b/app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js new file mode 100644 index 0000000..082e47c --- /dev/null +++ b/app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js @@ -0,0 +1,213 @@ +export class JobEntry +{ + id = ""; + description = ""; + message = ""; + progress = 0; + + dom = null; + + constructor(job) + { + this.id = job.id; + this.description = job.description; + this.message = job.message; + this.progress = job.progress; + } + + createDom(template, parent) + { + this.dom = template.clone(); + this.dom.attr('id', `job_${this.id}`); + this.dom.addClass('job_entry'); + this.dom.removeClass('collapse'); + + this.updateDom(); + + this.dom.appendTo(parent); + } + + update(job) + { + if (job !== null) { + this.description = job.description; + this.message = job.message; + this.progress = job.progress; + } + + this.updateDom(); + } + + updateDom() + { + if (this.dom === null) { + return; + } + this.dom.find('#job_panel_item_title').text(this.description); + this.dom.find('#job_panel_item_subtitle').text(this.message); + + let entryPercent = 100 * this.progress; + let jobEntryProgress = this.dom.find('#job_panel_item_progress'); + jobEntryProgress.width(entryPercent + '%'); + jobEntryProgress.text(`${entryPercent.toFixed(0)}%`); + } + + deleteDom() + { + if (this.dom === null) { + return; + } + this.dom.remove(); + this.dom = null; + } +} + +export class JobPanel +{ + static QUERY_INTERVAL = 1500; + + statusBar_Progress = null; + panel = null; + panel_Title = null; + panel_TitleNoJobs = null; + panel_JobTemplate = null; + + jobs = []; + + constructor() + { + this.statusBar_Progress = $('#status-progress'); + this.panel = $('#job_panel'); + this.panel_Title = this.panel.find('#job_panel_title'); + this.panel_TitleNoJobs = this.panel.find('#job_panel_no_jobs_title'); + this.panel_JobTemplate = this.panel.find('#job_panel_item_template'); + } + + update() + { + let pThis = this; + + $.get(window.ytsmContext.url_ajax_get_running_jobs) + .done(function(data, textStatus, jqXHR) { + if (jqXHR.getResponseHeader('content-type') === "application/json") { + pThis._updateInternal(data); + } + else { + pThis._clear(); + } + }); + } + + _updateInternal(data) + { + this._updateJobs(data); + this._updateStatusBar(); + this._updateProgressBar(); + this._updateTitle(); + + $('#btn_toggle_job_panel').dropdown('update'); + } + + _updateJobs(data) + { + let keep = []; + + for (let srvJob of data) + { + let found = false; + + // Find existing jobs + for (let job of this.jobs) { + if (job.id === srvJob.id) { + job.update(srvJob); + found = true; + keep.push(job.id); + } + } + + // New job + if (!found) { + let job = new JobEntry(srvJob); + job.createDom(this.panel_JobTemplate, this.panel); + this.jobs.push(job); + keep.push(job.id); + } + } + + // Delete old jobs + for (let i = 0; i < this.jobs.length; i++) { + if (keep.indexOf(this.jobs[i].id) < 0) { + this.jobs[i].deleteDom(); + this.jobs.splice(i--, 1); + } + } + } + + _clear() + { + // Delete old jobs + for (let i = 0; i < this.jobs.length; i++) { + this.jobs[i].deleteDom(); + } + this.jobs = []; + } + + _updateTitle() + { + if (this.jobs.length === 0) { + this.panel_Title.addClass('collapse'); + this.panel_TitleNoJobs.removeClass('collapse'); + } + else { + this.panel_Title.removeClass('collapse'); + this.panel_TitleNoJobs.addClass('collapse'); + } + } + + _updateStatusBar() + { + let text = ""; + + if (this.jobs.length === 1) { + text = `${this.jobs[0].description} | ${this.jobs[0].message}`; + } + else if (this.jobs.length > 1) { + text = `Running ${this.jobs.length} jobs...`; + } + $('#status-message').text(text); + } + + _updateProgressBar() + { + if (this.jobs.length > 0) { + // Make visible + this.statusBar_Progress.removeClass('invisible'); + + // Calculate progress + let combinedProgress = 0; + for (let job of this.jobs) { + combinedProgress += job.progress; + } + + let percent = 100 * combinedProgress / this.jobs.length; + + let bar = this.statusBar_Progress.find('.progress-bar'); + bar.width(percent + '%'); + bar.text(`${percent.toFixed(0)}%`); + } + else { + // hide + this.statusBar_Progress.addClass('invisible'); + } + } + + enable() + { + this.update(); + + let pThis = this; + setInterval(function() { + pThis.update(); + }, JobPanel.QUERY_INTERVAL); + } +} \ No newline at end of file diff --git a/app/YtManagerApp/templates/YtManagerApp/index.html b/app/YtManagerApp/templates/YtManagerApp/index.html index 46ef6d9..c18de29 100644 --- a/app/YtManagerApp/templates/YtManagerApp/index.html +++ b/app/YtManagerApp/templates/YtManagerApp/index.html @@ -33,15 +33,21 @@