mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Began refactoring javascript code
This commit is contained in:
parent
8fa67a81d8
commit
6afca61dd9
@ -1,12 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Union
|
from typing import List, Dict, Union, Iterable
|
||||||
|
|
||||||
from YtManagerApp.models import VideoProviderConfig, Video, Subscription
|
from YtManagerApp.models import VideoProviderConfig, Video, Subscription
|
||||||
from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError
|
from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError
|
||||||
import json
|
import json
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
log = logging.getLogger("VideoProviderManager")
|
log = logging.getLogger("VideoProviderManager")
|
||||||
|
|
||||||
|
VideoProviderInfo = namedtuple('VideoProviderInfo', ['id', 'name', 'is_configured', 'description'])
|
||||||
|
|
||||||
|
|
||||||
class VideoProviderManager(object):
|
class VideoProviderManager(object):
|
||||||
def __init__(self, registered_providers: List[VideoProvider]):
|
def __init__(self, registered_providers: List[VideoProvider]):
|
||||||
@ -23,18 +26,18 @@ class VideoProviderManager(object):
|
|||||||
:param provider: Video provider
|
:param provider: Video provider
|
||||||
"""
|
"""
|
||||||
# avoid duplicates
|
# avoid duplicates
|
||||||
if provider.name in self._registered_providers:
|
if provider.id in self._registered_providers:
|
||||||
log.error(f"Duplicate video provider {provider.name}")
|
log.error(f"Duplicate video provider {provider.id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# register
|
# register
|
||||||
self._registered_providers[provider.name] = provider
|
self._registered_providers[provider.id] = provider
|
||||||
log.info(f"Registered video provider {provider.name}")
|
log.info(f"Registered video provider {provider.id}")
|
||||||
|
|
||||||
# load configuration (if any)
|
# load configuration (if any)
|
||||||
if provider.name in self._pending_configs:
|
if provider.id in self._pending_configs:
|
||||||
self._configure(provider, self._pending_configs[provider.name])
|
self._configure(provider, self._pending_configs[provider.id])
|
||||||
del self._pending_configs[provider.name]
|
del self._pending_configs[provider.id]
|
||||||
|
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
# Loads configuration from database
|
# Loads configuration from database
|
||||||
@ -53,8 +56,8 @@ class VideoProviderManager(object):
|
|||||||
def _configure(self, provider, config):
|
def _configure(self, provider, config):
|
||||||
settings = json.loads(config.settings)
|
settings = json.loads(config.settings)
|
||||||
provider.configure(settings)
|
provider.configure(settings)
|
||||||
log.info(f"Configured video provider {provider.name}")
|
log.info(f"Configured video provider {provider.id}")
|
||||||
self._configured_providers[provider.name] = provider
|
self._configured_providers[provider.id] = provider
|
||||||
|
|
||||||
def get(self, item: Union[str, Subscription, Video]):
|
def get(self, item: Union[str, Subscription, Video]):
|
||||||
"""
|
"""
|
||||||
@ -100,3 +103,14 @@ class VideoProviderManager(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
raise InvalidURLError("The given URL is not valid for any of the supported sites!")
|
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))
|
||||||
|
47
app/YtManagerApp/providers/dummy_video_provider.py
Normal file
47
app/YtManagerApp/providers/dummy_video_provider.py
Normal file
@ -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
|
@ -6,7 +6,7 @@ from django.forms import Field
|
|||||||
from YtManagerApp.models import Subscription, Video
|
from YtManagerApp.models import Subscription, Video
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationValidationError(ValueError):
|
class ProviderValidationError(ValueError):
|
||||||
"""
|
"""
|
||||||
Exception type thrown when validating configurations.
|
Exception type thrown when validating configurations.
|
||||||
"""
|
"""
|
||||||
@ -27,7 +27,21 @@ class InvalidURLError(ValueError):
|
|||||||
|
|
||||||
|
|
||||||
class VideoProvider(ABC):
|
class VideoProvider(ABC):
|
||||||
|
"""
|
||||||
|
Identifier
|
||||||
|
"""
|
||||||
|
id: str = ""
|
||||||
|
"""
|
||||||
|
Display name, shown to users
|
||||||
|
"""
|
||||||
name: str = ""
|
name: str = ""
|
||||||
|
"""
|
||||||
|
Description, shown to users
|
||||||
|
"""
|
||||||
|
description: str = ""
|
||||||
|
"""
|
||||||
|
Dictionary containing fields necessary for configuring
|
||||||
|
"""
|
||||||
settings: Dict[str, Field] = {}
|
settings: Dict[str, Field] = {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -9,7 +9,9 @@ from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError
|
|||||||
|
|
||||||
|
|
||||||
class YouTubeApiVideoProvider(VideoProvider):
|
class YouTubeApiVideoProvider(VideoProvider):
|
||||||
name = "YtAPI"
|
id = "YtAPI"
|
||||||
|
name = "YouTube API"
|
||||||
|
description = "Allows communication with YouTube using the YouTube API."
|
||||||
settings = {
|
settings = {
|
||||||
"api_key": forms.CharField(label="YouTube API Key:")
|
"api_key": forms.CharField(label="YouTube API Key:")
|
||||||
}
|
}
|
||||||
@ -44,7 +46,7 @@ class YouTubeApiVideoProvider(VideoProvider):
|
|||||||
|
|
||||||
def fetch_subscription(self, url: str) -> Subscription:
|
def fetch_subscription(self, url: str) -> Subscription:
|
||||||
sub = Subscription()
|
sub = Subscription()
|
||||||
sub.provider_id = self.name
|
sub.provider_id = self.id
|
||||||
|
|
||||||
self.validate_subscription_url(url)
|
self.validate_subscription_url(url)
|
||||||
url_parsed = self.__api.parse_url(url)
|
url_parsed = self.__api.parse_url(url)
|
||||||
|
@ -12,7 +12,9 @@ from YtManagerApp.scheduler.scheduler import YtsmScheduler
|
|||||||
|
|
||||||
class VideoProviders(containers.DeclarativeContainer):
|
class VideoProviders(containers.DeclarativeContainer):
|
||||||
from YtManagerApp.providers.ytapi_video_provider import YouTubeApiVideoProvider
|
from YtManagerApp.providers.ytapi_video_provider import YouTubeApiVideoProvider
|
||||||
|
from YtManagerApp.providers.dummy_video_provider import DummyVideoProvider
|
||||||
ytApiProvider = providers.Factory(YouTubeApiVideoProvider)
|
ytApiProvider = providers.Factory(YouTubeApiVideoProvider)
|
||||||
|
dummyProvider = providers.Factory(DummyVideoProvider)
|
||||||
|
|
||||||
|
|
||||||
class Services(containers.DeclarativeContainer):
|
class Services(containers.DeclarativeContainer):
|
||||||
@ -21,6 +23,9 @@ class Services(containers.DeclarativeContainer):
|
|||||||
scheduler = providers.Singleton(YtsmScheduler, appConfig)
|
scheduler = providers.Singleton(YtsmScheduler, appConfig)
|
||||||
youtubeDLManager = providers.Singleton(YoutubeDlManager)
|
youtubeDLManager = providers.Singleton(YoutubeDlManager)
|
||||||
videoManager = providers.Singleton(VideoManager)
|
videoManager = providers.Singleton(VideoManager)
|
||||||
videoProviderManager = providers.Singleton(VideoProviderManager, [VideoProviders.ytApiProvider()])
|
videoProviderManager = providers.Singleton(VideoProviderManager, [
|
||||||
|
VideoProviders.ytApiProvider(),
|
||||||
|
VideoProviders.dummyProvider(),
|
||||||
|
])
|
||||||
subscriptionManager = providers.Singleton(SubscriptionManager)
|
subscriptionManager = providers.Singleton(SubscriptionManager)
|
||||||
downloadManager = providers.Singleton(DownloadManager)
|
downloadManager = providers.Singleton(DownloadManager)
|
||||||
|
@ -1,18 +1,42 @@
|
|||||||
#main_body {
|
:root {
|
||||||
margin-bottom: 4rem;
|
--blue: #007bff;
|
||||||
margin-top: 0; }
|
--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 {
|
[data-theme="dark"] {
|
||||||
position: fixed;
|
--primary: #007bff;
|
||||||
left: 0;
|
--secondary: #6c757d;
|
||||||
right: 0;
|
--success: #28a745;
|
||||||
bottom: 0;
|
--info: #17a2b8;
|
||||||
height: 2rem;
|
--warning: #ffc107;
|
||||||
line-height: 2rem;
|
--danger: #dc3545;
|
||||||
padding: 0 1rem;
|
--light: #343a40;
|
||||||
display: flex;
|
--dark: #f8f9fa; }
|
||||||
align-content: center;
|
|
||||||
font-size: 10pt; }
|
|
||||||
|
|
||||||
/* Loading animation */
|
/* Loading animation */
|
||||||
.loading-dual-ring {
|
.loading-dual-ring {
|
||||||
@ -26,8 +50,8 @@
|
|||||||
height: 46px;
|
height: 46px;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 5px solid #007bff;
|
border: 5px solid var(--primary);
|
||||||
border-color: #007bff transparent #007bff transparent;
|
border-color: var(--primary) transparent var(--primary) transparent;
|
||||||
animation: loading-dual-ring 1.2s linear infinite; }
|
animation: loading-dual-ring 1.2s linear infinite; }
|
||||||
|
|
||||||
.loading-dual-ring-small {
|
.loading-dual-ring-small {
|
||||||
@ -41,8 +65,8 @@
|
|||||||
height: 23px;
|
height: 23px;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2.5px solid #007bff;
|
border: 2.5px solid var(--primary);
|
||||||
border-color: #007bff transparent #007bff transparent;
|
border-color: var(--primary) transparent var(--primary) transparent;
|
||||||
animation: loading-dual-ring 1.2s linear infinite; }
|
animation: loading-dual-ring 1.2s linear infinite; }
|
||||||
|
|
||||||
@keyframes loading-dual-ring {
|
@keyframes loading-dual-ring {
|
||||||
@ -57,6 +81,84 @@
|
|||||||
margin-top: -32px;
|
margin-top: -32px;
|
||||||
margin-left: -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 {
|
.black-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
/* Sit on top of the page content */
|
/* Sit on top of the page content */
|
||||||
@ -168,4 +270,20 @@
|
|||||||
img.muted {
|
img.muted {
|
||||||
opacity: .5; }
|
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 */
|
/*# sourceMappingURL=style.css.map */
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"mappings": "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",
|
"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": ["style.scss"],
|
"sources": ["lib/_theme.scss","lib/_loading.scss","lib/_hamburger.scss","style.scss"],
|
||||||
"names": [],
|
"names": [],
|
||||||
"file": "style.css"
|
"file": "style.css"
|
||||||
}
|
}
|
@ -1,8 +1,20 @@
|
|||||||
|
@import "lib/theme";
|
||||||
|
@import "lib/loading";
|
||||||
|
@import "lib/hamburger";
|
||||||
|
|
||||||
$accent-color: #007bff;
|
$accent-color: #007bff;
|
||||||
|
$success-color: #28a745;
|
||||||
|
|
||||||
|
#main_navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
#main_body {
|
#main_body {
|
||||||
margin-bottom: 4rem;
|
margin-bottom: 4rem;
|
||||||
margin-top: 0;
|
margin-top: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main_footer {
|
#main_footer {
|
||||||
@ -18,50 +30,6 @@ $accent-color: #007bff;
|
|||||||
font-size: 10pt;
|
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 {
|
.black-overlay {
|
||||||
position: fixed; /* Sit on top of the page content */
|
position: fixed; /* Sit on top of the page content */
|
||||||
display: none; /* Hidden by default */
|
display: none; /* Hidden by default */
|
||||||
@ -212,4 +180,24 @@ $accent-color: #007bff;
|
|||||||
|
|
||||||
img.muted {
|
img.muted {
|
||||||
opacity: .5;
|
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;
|
||||||
}
|
}
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
28
app/YtManagerApp/static/YtManagerApp/js/common.js
Normal file
28
app/YtManagerApp/static/YtManagerApp/js/common.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {JobPanel} from "./components/JobPanel.js";
|
||||||
|
import {AjaxModal} from "./components/AjaxModal.js";
|
||||||
|
|
||||||
|
// Document loaded
|
||||||
|
jQuery(function() {
|
||||||
|
// Setup job panel
|
||||||
|
window.ytsm_JobPanel = new JobPanel();
|
||||||
|
window.ytsm_JobPanel.enable();
|
||||||
|
|
||||||
|
// Setup hamburger menu
|
||||||
|
$('#hamburger-button').on('click', function() {
|
||||||
|
$('#hamburger').toggleClass('hamburger-show');
|
||||||
|
$('#hamburger-button').toggleClass('hamburger-show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize modals
|
||||||
|
$('[data-modal="modal"]').on('click', function() {
|
||||||
|
let callbackStr = $(this).data('modal-callback');
|
||||||
|
let callback = eval(callbackStr);
|
||||||
|
|
||||||
|
let modal = new AjaxModal($(this).data('modal-url'));
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
modal.submitCallback = callback;
|
||||||
|
}
|
||||||
|
modal.loadAndShow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
167
app/YtManagerApp/static/YtManagerApp/js/components/AjaxModal.js
Normal file
167
app/YtManagerApp/static/YtManagerApp/js/components/AjaxModal.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
export class AjaxModal
|
||||||
|
{
|
||||||
|
wrapper = null;
|
||||||
|
loading = null;
|
||||||
|
url = "";
|
||||||
|
modal = null;
|
||||||
|
form = null;
|
||||||
|
modalLoadingRing = null;
|
||||||
|
|
||||||
|
submitCallback = null;
|
||||||
|
|
||||||
|
constructor(url) {
|
||||||
|
this.wrapper = $("#modal-wrapper");
|
||||||
|
this.loading = $("#modal-loading");
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showLoading() {
|
||||||
|
this.loading.fadeIn(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideLoading() {
|
||||||
|
this.loading.fadeOut(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
_showModal() {
|
||||||
|
if (this.modal != null)
|
||||||
|
this.modal.modal();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideModal() {
|
||||||
|
if (this.modal != null)
|
||||||
|
this.modal.modal('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
_load(result) {
|
||||||
|
this.wrapper.html(result);
|
||||||
|
|
||||||
|
this.modal = this.wrapper.find('.modal');
|
||||||
|
this.form = this.wrapper.find('form');
|
||||||
|
this.modalLoadingRing = this.wrapper.find('#modal-loading-ring');
|
||||||
|
|
||||||
|
let pThis = this;
|
||||||
|
this.form.on("submit", function(e) {
|
||||||
|
pThis._submit(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadFailed() {
|
||||||
|
this.wrapper.html('<div class="alert alert-danger">An error occurred while displaying the dialog!</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 = "<div class=\"alert alert-danger modal-field-error\"><ul>";
|
||||||
|
|
||||||
|
for(let error of errorsArray) {
|
||||||
|
errorsConcat += `<li>${error.message}</li>`;
|
||||||
|
}
|
||||||
|
errorsConcat += '</ul></div>';
|
||||||
|
|
||||||
|
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(`<div class="alert alert-danger modal-field-error">An error occurred while processing request!</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_submitInvalidResponse() {
|
||||||
|
// Clear old errors first
|
||||||
|
this.form.find('.modal-field-error').remove();
|
||||||
|
this.form.find('.modal-body')
|
||||||
|
.append(`<div class="alert alert-danger modal-field-error">Invalid server response!</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
213
app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js
Normal file
213
app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -33,15 +33,21 @@
|
|||||||
<div class="btn-toolbar" role="toolbar" aria-label="Subscriptions toolbar">
|
<div class="btn-toolbar" role="toolbar" aria-label="Subscriptions toolbar">
|
||||||
<div class="btn-group mr-2" role="group">
|
<div class="btn-group mr-2" role="group">
|
||||||
<button id="btn_create_sub" type="button" class="btn btn-light"
|
<button id="btn_create_sub" type="button" class="btn btn-light"
|
||||||
data-toggle="tooltip" title="Add subscription...">
|
data-toggle="tooltip" title="Add subscription..."
|
||||||
|
data-modal="modal" data-modal-url="{% url 'modal_create_subscription' %}"
|
||||||
|
data-modal-callback="tree_Refresh">
|
||||||
<span class="typcn typcn-plus" aria-hidden="true"></span>
|
<span class="typcn typcn-plus" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn_create_folder" type="button" class="btn btn-light"
|
<button id="btn_create_folder" type="button" class="btn btn-light"
|
||||||
data-toggle="tooltip" title="Add folder...">
|
data-toggle="tooltip" title="Add folder..."
|
||||||
|
data-modal="modal" data-modal-url="{% url 'modal_create_folder' %}"
|
||||||
|
data-modal-callback="tree_Refresh">
|
||||||
<span class="typcn typcn-folder-add" aria-hidden="true"></span>
|
<span class="typcn typcn-folder-add" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn_import" type="button" class="btn btn-light"
|
<button id="btn_import" type="button" class="btn btn-light"
|
||||||
data-toggle="tooltip" title="Import from file...">
|
data-toggle="tooltip" title="Import from file..."
|
||||||
|
data-modal="modal" data-modal-url="{% url 'modal_import_subscriptions' %}"
|
||||||
|
data-modal-callback="tree_Refresh">
|
||||||
<span class="typcn typcn-document-add" aria-hidden="true"></span>
|
<span class="typcn typcn-document-add" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,175 +6,6 @@ function zeroFill(number, width) {
|
|||||||
return number + ""; // always return a string
|
return number + ""; // always return a string
|
||||||
}
|
}
|
||||||
|
|
||||||
class AjaxModal
|
|
||||||
{
|
|
||||||
constructor(url)
|
|
||||||
{
|
|
||||||
this.wrapper = $("#modal-wrapper");
|
|
||||||
this.loading = $("#modal-loading");
|
|
||||||
this.url = url;
|
|
||||||
this.modal = null;
|
|
||||||
this.form = null;
|
|
||||||
this.submitCallback = null;
|
|
||||||
this.modalLoadingRing = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitCallback(callback) {
|
|
||||||
this.submitCallback = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showLoading() {
|
|
||||||
this.loading.fadeIn(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
_hideLoading() {
|
|
||||||
this.loading.fadeOut(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
_showModal() {
|
|
||||||
if (this.modal != null)
|
|
||||||
this.modal.modal();
|
|
||||||
}
|
|
||||||
|
|
||||||
_hideModal() {
|
|
||||||
if (this.modal != null)
|
|
||||||
this.modal.modal('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
_load(result) {
|
|
||||||
this.wrapper.html(result);
|
|
||||||
|
|
||||||
this.modal = this.wrapper.find('.modal');
|
|
||||||
this.form = this.wrapper.find('form');
|
|
||||||
this.modalLoadingRing = this.wrapper.find('#modal-loading-ring');
|
|
||||||
|
|
||||||
let pThis = this;
|
|
||||||
this.form.submit(function(e) {
|
|
||||||
pThis._submit(e);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadFailed() {
|
|
||||||
this.wrapper.html('<div class="alert alert-danger">An error occurred while displaying the dialog!</div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
_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)
|
|
||||||
if (result.errors.hasOwnProperty(field))
|
|
||||||
{
|
|
||||||
let errorsArray = result.errors[field];
|
|
||||||
let errorsConcat = "<div class=\"alert alert-danger modal-field-error\"><ul>";
|
|
||||||
|
|
||||||
for(let error of errorsArray) {
|
|
||||||
errorsConcat += `<li>${error.message}</li>`;
|
|
||||||
}
|
|
||||||
errorsConcat += '</ul></div>';
|
|
||||||
|
|
||||||
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(`<div class="alert alert-danger modal-field-error">An error occurred while processing request!</div>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
_submitInvalidResponse() {
|
|
||||||
// Clear old errors first
|
|
||||||
this.form.find('.modal-field-error').remove();
|
|
||||||
this.form.find('.modal-body')
|
|
||||||
.append(`<div class="alert alert-danger modal-field-error">Invalid server response!</div>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncNow() {
|
function syncNow() {
|
||||||
$.post("{% url 'ajax_action_sync_now' %}", {
|
$.post("{% url 'ajax_action_sync_now' %}", {
|
||||||
csrfmiddlewaretoken: '{{ csrf_token }}'
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
||||||
|
@ -175,84 +175,6 @@ function videos_Submit(e)
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Notifications
|
|
||||||
///
|
|
||||||
const JOB_QUERY_INTERVAL = 1500;
|
|
||||||
|
|
||||||
|
|
||||||
function get_and_process_running_jobs()
|
|
||||||
{
|
|
||||||
$.get("{% url 'ajax_get_running_jobs' %}")
|
|
||||||
.done(function(data) {
|
|
||||||
|
|
||||||
let progress = $('#status-progress');
|
|
||||||
let jobPanel = $('#job_panel');
|
|
||||||
let jobTitle = jobPanel.find('#job_panel_title');
|
|
||||||
let jobTitleNoJobs = jobPanel.find('#job_panel_no_jobs_title');
|
|
||||||
let jobTemplate = jobPanel.find('#job_panel_item_template');
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
|
|
||||||
// Update status bar
|
|
||||||
if (data.length > 1) {
|
|
||||||
$('#status-message').text(`Running ${data.length} jobs...`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('#status-message').text(`${data[0].description} | ${data[0].message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update global progress bar
|
|
||||||
let combinedProgress = 0;
|
|
||||||
for (let entry of data) {
|
|
||||||
combinedProgress += entry.progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
let percent = 100 * combinedProgress / data.length;
|
|
||||||
|
|
||||||
progress.removeClass('invisible');
|
|
||||||
let bar = progress.find('.progress-bar');
|
|
||||||
bar.width(percent + '%');
|
|
||||||
bar.text(`${percent.toFixed(0)}%`);
|
|
||||||
|
|
||||||
// Update entries in job list
|
|
||||||
jobTitle.removeClass('collapse');
|
|
||||||
jobTitleNoJobs.addClass('collapse');
|
|
||||||
|
|
||||||
data.sort(function (a, b) { return a.id - b.id });
|
|
||||||
jobPanel.find('.job_entry').remove();
|
|
||||||
|
|
||||||
for (let entry of data) {
|
|
||||||
let jobEntry = jobTemplate.clone();
|
|
||||||
jobEntry.attr('id', `job_${entry.id}`);
|
|
||||||
jobEntry.addClass('job_entry');
|
|
||||||
jobEntry.removeClass('collapse');
|
|
||||||
jobEntry.find('#job_panel_item_title').text(entry.description);
|
|
||||||
jobEntry.find('#job_panel_item_subtitle').text(entry.message);
|
|
||||||
|
|
||||||
let entryPercent = 100 * entry.progress;
|
|
||||||
let jobEntryProgress = jobEntry.find('#job_panel_item_progress');
|
|
||||||
jobEntryProgress.width(entryPercent + '%');
|
|
||||||
jobEntryProgress.text(`${entryPercent.toFixed(0)}%`);
|
|
||||||
|
|
||||||
jobEntry.appendTo(jobPanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#btn_toggle_job_panel').dropdown('update');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
progress.addClass('invisible');
|
|
||||||
$('#status-message').text("");
|
|
||||||
|
|
||||||
jobTitle.addClass('collapse');
|
|
||||||
jobTitleNoJobs.removeClass('collapse');
|
|
||||||
jobPanel.find('.job_entry').remove();
|
|
||||||
|
|
||||||
$('#btn_toggle_job_panel').dropdown('update');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Initialization
|
/// Initialization
|
||||||
///
|
///
|
||||||
@ -263,22 +185,22 @@ $(document).ready(function ()
|
|||||||
|
|
||||||
tree_Initialize();
|
tree_Initialize();
|
||||||
|
|
||||||
// Subscription toolbar
|
// // Subscription toolbar
|
||||||
$("#btn_create_sub").on("click", function () {
|
// $("#btn_create_sub").on("click", function () {
|
||||||
let modal = new AjaxModal("{% url 'modal_create_subscription' %}");
|
// let modal = new AjaxModal("{% url 'modal_create_subscription' %}");
|
||||||
modal.setSubmitCallback(tree_Refresh);
|
// modal.setSubmitCallback(tree_Refresh);
|
||||||
modal.loadAndShow();
|
// modal.loadAndShow();
|
||||||
});
|
// });
|
||||||
$("#btn_create_folder").on("click", function () {
|
// $("#btn_create_folder").on("click", function () {
|
||||||
let modal = new AjaxModal("{% url 'modal_create_folder' %}");
|
// let modal = new AjaxModal("{% url 'modal_create_folder' %}");
|
||||||
modal.setSubmitCallback(tree_Refresh);
|
// modal.setSubmitCallback(tree_Refresh);
|
||||||
modal.loadAndShow();
|
// modal.loadAndShow();
|
||||||
});
|
// });
|
||||||
$("#btn_import").on("click", function () {
|
// $("#btn_import").on("click", function () {
|
||||||
let modal = new AjaxModal("{% url 'modal_import_subscriptions' %}");
|
// let modal = new AjaxModal("{% url 'modal_import_subscriptions' %}");
|
||||||
modal.setSubmitCallback(tree_Refresh);
|
// modal.setSubmitCallback(tree_Refresh);
|
||||||
modal.loadAndShow();
|
// modal.loadAndShow();
|
||||||
});
|
// });
|
||||||
$("#btn_edit_node").on("click", treeNode_Edit);
|
$("#btn_edit_node").on("click", treeNode_Edit);
|
||||||
$("#btn_delete_node").on("click", treeNode_Delete);
|
$("#btn_delete_node").on("click", treeNode_Delete);
|
||||||
|
|
||||||
@ -292,8 +214,4 @@ $(document).ready(function ()
|
|||||||
filters_form.find('select[name=results_per_page]').on('change', videos_ResetPageAndReloadWithTimer);
|
filters_form.find('select[name=results_per_page]').on('change', videos_ResetPageAndReloadWithTimer);
|
||||||
|
|
||||||
videos_Reload();
|
videos_Reload();
|
||||||
|
|
||||||
// Notifications
|
|
||||||
get_and_process_running_jobs();
|
|
||||||
setInterval(get_and_process_running_jobs, JOB_QUERY_INTERVAL);
|
|
||||||
});
|
});
|
||||||
|
@ -11,55 +11,67 @@
|
|||||||
<link rel="stylesheet" href="{% static 'YtManagerApp/import/bootstrap/css/bootstrap.min.css' %}">
|
<link rel="stylesheet" href="{% static 'YtManagerApp/import/bootstrap/css/bootstrap.min.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'YtManagerApp/import/typicons/typicons.min.css' %}" />
|
<link rel="stylesheet" href="{% static 'YtManagerApp/import/typicons/typicons.min.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'YtManagerApp/css/style.css' %}">
|
<link rel="stylesheet" href="{% static 'YtManagerApp/css/style.css' %}">
|
||||||
|
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav id="main_navbar" class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
<a class="navbar-brand" href="{% url 'home' %}">YouTube Manager</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
|
|
||||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
{% if request.user.is_authenticated %}
|
||||||
<ul class="navbar-nav ml-auto">
|
<button id="hamburger-button"
|
||||||
{% if request.user.is_authenticated %}
|
type="button"
|
||||||
<li class="nav-item dropdown">
|
class="btn btn-light">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
|
<span></span>
|
||||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<span></span>
|
||||||
Welcome,
|
<span></span>
|
||||||
{% if request.user.first_name %}
|
</button>
|
||||||
{{ request.user.first_name }}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
{{ request.user.username }}
|
<a class="navbar-brand" href="{% url 'home' %}">YouTube Manager</a>
|
||||||
{% endif %}
|
|
||||||
</a>
|
{% block navbar-content %}
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userDropdown">
|
{% endblock %}
|
||||||
<a class="dropdown-item" href="{% url 'settings' %}">Settings</a>
|
|
||||||
{% if request.user.is_superuser %}
|
<ul class="navbar-nav ml-auto">
|
||||||
<a class="dropdown-item" href="{% url 'admin_settings' %}">Admin settings</a>
|
{% if not request.user.is_authenticated %}
|
||||||
{% endif %}
|
<li class="nav-item">
|
||||||
<div class="dropdown-divider"></div>
|
<a class="nav-link" href="{% url 'login' %}">Login</a>
|
||||||
<a class="dropdown-item" href="{% url 'logout' %}">Log out</a>
|
</li>
|
||||||
</div>
|
{% if global_preferences.general__allow_registrations %}
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'login' %}">Login</a>
|
<a class="nav-link" href="{% url 'register' %}">Register</a>
|
||||||
</li>
|
</li>
|
||||||
{% if global_preferences.general__allow_registrations %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'register' %}">Register</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
{% endif %}
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<div id="hamburger">
|
||||||
|
<div id="hamburger-content">
|
||||||
|
{% block hamburger-menu %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div id="hamburger-footer">
|
||||||
|
<div class="btn-toolbar row">
|
||||||
|
<a class="btn btn-light col"
|
||||||
|
href="{% url 'settings' %}"
|
||||||
|
data-toggle="tooltip" title="Settings">
|
||||||
|
<span class="typcn typcn-cog"></span>Settings
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-light col"
|
||||||
|
href="{% url 'logout' %}"
|
||||||
|
data-toggle="tooltip" title="Log out">
|
||||||
|
<span class="typcn typcn-eject"></span>Log out
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div id="main_body" class="container-fluid">
|
<div id="main_body" class="container-fluid">
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -77,7 +89,7 @@
|
|||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button id="btn_toggle_job_panel"
|
<button id="btn_toggle_job_panel"
|
||||||
class="btn btn-sm btn-light dropdown-toggle"
|
class="btn btn-sm btn-light dropdown-toggle"
|
||||||
title="Show/hide details"
|
title="Show/hide job details"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
aria-haspopup="true" aria-expanded="false">
|
aria-haspopup="true" aria-expanded="false">
|
||||||
<span class="typcn typcn-th-list" aria-hidden="true"></span>
|
<span class="typcn typcn-th-list" aria-hidden="true"></span>
|
||||||
@ -110,6 +122,18 @@
|
|||||||
<script src="{% static 'YtManagerApp/import/jquery/jquery-3.3.1.min.js' %}"></script>
|
<script src="{% static 'YtManagerApp/import/jquery/jquery-3.3.1.min.js' %}"></script>
|
||||||
<script src="{% static 'YtManagerApp/import/popper/popper.min.js' %}"></script>
|
<script src="{% static 'YtManagerApp/import/popper/popper.min.js' %}"></script>
|
||||||
<script src="{% static 'YtManagerApp/import/bootstrap/js/bootstrap.min.js' %}"></script>
|
<script src="{% static 'YtManagerApp/import/bootstrap/js/bootstrap.min.js' %}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.ytsmContext = {
|
||||||
|
csrf_token: "{{ csrf_token }}",
|
||||||
|
url_ajax_get_running_jobs: "{% url 'ajax_get_running_jobs' %}",
|
||||||
|
{% block context %}
|
||||||
|
{% endblock %}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module" src="{% static 'YtManagerApp/js/common.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
{% include 'YtManagerApp/js/common.js' %}
|
{% include 'YtManagerApp/js/common.js' %}
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "YtManagerApp/master_default.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
{% for provider in providers %}
|
||||||
|
{% if provider.is_configured %}
|
||||||
|
<div class="provider-wrapper col-2">
|
||||||
|
<button class="btn btn-light ">
|
||||||
|
<img class="provider-logo"
|
||||||
|
src="{% static provider.image_src %}"
|
||||||
|
title="{{ provider.name }}">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if have_unconfigured %}
|
||||||
|
<div class="provider-wrapper col-2 dropdown">
|
||||||
|
<button id="btnAddProvider"
|
||||||
|
class="btn btn-light"
|
||||||
|
type="button"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="provider-logo typcn typcn-plus"
|
||||||
|
title="Add provider">
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -18,6 +18,7 @@ from django.conf.urls import include
|
|||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from YtManagerApp.views.settings.providers_view import ProvidersView
|
||||||
from .views import first_time
|
from .views import first_time
|
||||||
from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \
|
from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \
|
||||||
MarkVideoUnwatchedView
|
MarkVideoUnwatchedView
|
||||||
@ -25,7 +26,7 @@ from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
|
|||||||
from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal, \
|
from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal, \
|
||||||
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal
|
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal
|
||||||
from .views.notifications import ajax_get_running_jobs
|
from .views.notifications import ajax_get_running_jobs
|
||||||
from .views.settings import SettingsView, AdminSettingsView
|
from YtManagerApp.views.settings.settings import SettingsView, AdminSettingsView
|
||||||
from .views.video import VideoDetailView, video_detail_view
|
from .views.video import VideoDetailView, video_detail_view
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -63,6 +64,7 @@ urlpatterns = [
|
|||||||
# Pages
|
# Pages
|
||||||
path('', index, name='home'),
|
path('', index, name='home'),
|
||||||
path('settings/', SettingsView.as_view(), name='settings'),
|
path('settings/', SettingsView.as_view(), name='settings'),
|
||||||
|
path('settings/providers/', ProvidersView.as_view(), name='settings_providers'),
|
||||||
path('admin_settings/', AdminSettingsView.as_view(), name='admin_settings'),
|
path('admin_settings/', AdminSettingsView.as_view(), name='admin_settings'),
|
||||||
path('video/<int:pk>/', VideoDetailView.as_view(), name='video'),
|
path('video/<int:pk>/', VideoDetailView.as_view(), name='video'),
|
||||||
path('video-src/<int:pk>/', video_detail_view, name='video-src'),
|
path('video-src/<int:pk>/', video_detail_view, name='video-src'),
|
||||||
|
0
app/YtManagerApp/views/settings/__init__.py
Normal file
0
app/YtManagerApp/views/settings/__init__.py
Normal file
31
app/YtManagerApp/views/settings/providers_view.py
Normal file
31
app/YtManagerApp/views/settings/providers_view.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
from YtManagerApp.services import Services
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
VideoProviderInfoViewModel = namedtuple('VideoProviderInfoViewModel',
|
||||||
|
['id', 'name', 'is_configured', 'image_src'])
|
||||||
|
|
||||||
|
|
||||||
|
class ProvidersView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = 'YtManagerApp/settings/providers.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
providers = []
|
||||||
|
have_unconfigured = False
|
||||||
|
for provider in Services.videoProviderManager().get_available_providers():
|
||||||
|
providers.append(VideoProviderInfoViewModel(
|
||||||
|
id=provider.id,
|
||||||
|
name=provider.name,
|
||||||
|
is_configured=provider.is_configured,
|
||||||
|
image_src=f"YtManagerApp/img/video_providers/{provider.id}.png"
|
||||||
|
))
|
||||||
|
if not provider.is_configured:
|
||||||
|
have_unconfigured = True
|
||||||
|
|
||||||
|
context['providers'] = sorted(providers, key=lambda x: x.name)
|
||||||
|
context['have_unconfigured'] = have_unconfigured
|
||||||
|
return context
|
Loading…
Reference in New Issue
Block a user