Began refactoring javascript code

This commit is contained in:
Tiberiu Chibici 2020-04-23 00:47:27 +03:00
parent 8fa67a81d8
commit 6afca61dd9
22 changed files with 832 additions and 388 deletions

View File

@ -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))

View 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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 */

View File

@ -1,7 +1,7 @@
{ {
"version": 3, "version": 3,
"mappings": "AAEA,UAAW;EACP,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,CAAC;;AAGjB,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;;AAqBnB,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,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,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,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,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;;AAGtB,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,EA1Jb,OAAO;IA4Jd,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", "mappings": "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",
"sources": ["style.scss"], "sources": ["lib/_theme.scss","lib/_loading.scss","lib/_hamburger.scss","style.scss"],
"names": [], "names": [],
"file": "style.css" "file": "style.css"
} }

View File

@ -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 */
@ -213,3 +181,23 @@ $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

View 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();
});
});

View 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();
});
}
}

View 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);
}
}

View File

@ -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>

View File

@ -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 }}'

View File

@ -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);
}); });

View File

@ -11,41 +11,31 @@
<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">
<ul class="navbar-nav ml-auto">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="nav-item dropdown"> <button id="hamburger-button"
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> class="btn btn-light">
Welcome, <span></span>
{% if request.user.first_name %} <span></span>
{{ request.user.first_name }} <span></span>
{% else %} </button>
{{ request.user.username }}
{% endif %} {% endif %}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userDropdown"> <a class="navbar-brand" href="{% url 'home' %}">YouTube Manager</a>
<a class="dropdown-item" href="{% url 'settings' %}">Settings</a>
{% if request.user.is_superuser %} {% block navbar-content %}
<a class="dropdown-item" href="{% url 'admin_settings' %}">Admin settings</a> {% endblock %}
{% endif %}
<div class="dropdown-divider"></div> <ul class="navbar-nav ml-auto">
<a class="dropdown-item" href="{% url 'logout' %}">Log out</a> {% if not request.user.is_authenticated %}
</div>
</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 'login' %}">Login</a>
</li> </li>
@ -56,10 +46,32 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</ul> </ul>
</div>
</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>

View File

@ -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 %}

View File

@ -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'),

View 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