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:
		| @@ -1,12 +1,15 @@ | ||||
| import logging | ||||
| from typing import List, Dict, Union | ||||
| from typing import List, Dict, Union, Iterable | ||||
|  | ||||
| from YtManagerApp.models import VideoProviderConfig, Video, Subscription | ||||
| from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError | ||||
| import json | ||||
| from collections import namedtuple | ||||
|  | ||||
| log = logging.getLogger("VideoProviderManager") | ||||
|  | ||||
| VideoProviderInfo = namedtuple('VideoProviderInfo', ['id', 'name', 'is_configured', 'description']) | ||||
|  | ||||
|  | ||||
| class VideoProviderManager(object): | ||||
|     def __init__(self, registered_providers: List[VideoProvider]): | ||||
| @@ -23,18 +26,18 @@ class VideoProviderManager(object): | ||||
|         :param provider: Video provider | ||||
|         """ | ||||
|         # avoid duplicates | ||||
|         if provider.name in self._registered_providers: | ||||
|             log.error(f"Duplicate video provider {provider.name}") | ||||
|         if provider.id in self._registered_providers: | ||||
|             log.error(f"Duplicate video provider {provider.id}") | ||||
|             return | ||||
|  | ||||
|         # register | ||||
|         self._registered_providers[provider.name] = provider | ||||
|         log.info(f"Registered video provider {provider.name}") | ||||
|         self._registered_providers[provider.id] = provider | ||||
|         log.info(f"Registered video provider {provider.id}") | ||||
|  | ||||
|         # load configuration (if any) | ||||
|         if provider.name in self._pending_configs: | ||||
|             self._configure(provider, self._pending_configs[provider.name]) | ||||
|             del self._pending_configs[provider.name] | ||||
|         if provider.id in self._pending_configs: | ||||
|             self._configure(provider, self._pending_configs[provider.id]) | ||||
|             del self._pending_configs[provider.id] | ||||
|  | ||||
|     def _load(self) -> None: | ||||
|         # Loads configuration from database | ||||
| @@ -53,8 +56,8 @@ class VideoProviderManager(object): | ||||
|     def _configure(self, provider, config): | ||||
|         settings = json.loads(config.settings) | ||||
|         provider.configure(settings) | ||||
|         log.info(f"Configured video provider {provider.name}") | ||||
|         self._configured_providers[provider.name] = provider | ||||
|         log.info(f"Configured video provider {provider.id}") | ||||
|         self._configured_providers[provider.id] = provider | ||||
|  | ||||
|     def get(self, item: Union[str, Subscription, Video]): | ||||
|         """ | ||||
| @@ -100,3 +103,14 @@ class VideoProviderManager(object): | ||||
|                 pass | ||||
|  | ||||
|         raise InvalidURLError("The given URL is not valid for any of the supported sites!") | ||||
|  | ||||
|     def get_available_providers(self) -> Iterable[VideoProviderInfo]: | ||||
|         """ | ||||
|         Gets a list of available providers and some basic information about them. | ||||
|         :return: List of dictionary entries | ||||
|         """ | ||||
|         for key, provider in self._registered_providers.items(): | ||||
|             yield VideoProviderInfo(id=key, | ||||
|                                     name=provider.name, | ||||
|                                     description=provider.description, | ||||
|                                     is_configured=(key in self._configured_providers)) | ||||
|   | ||||
							
								
								
									
										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 | ||||
|  | ||||
|  | ||||
| class ConfigurationValidationError(ValueError): | ||||
| class ProviderValidationError(ValueError): | ||||
|     """ | ||||
|     Exception type thrown when validating configurations. | ||||
|     """ | ||||
| @@ -27,7 +27,21 @@ class InvalidURLError(ValueError): | ||||
|  | ||||
|  | ||||
| class VideoProvider(ABC): | ||||
|     """ | ||||
|     Identifier | ||||
|     """ | ||||
|     id: str = "" | ||||
|     """ | ||||
|     Display name, shown to users | ||||
|     """ | ||||
|     name: str = "" | ||||
|     """ | ||||
|     Description, shown to users | ||||
|     """ | ||||
|     description: str = "" | ||||
|     """ | ||||
|     Dictionary containing fields necessary for configuring | ||||
|     """ | ||||
|     settings: Dict[str, Field] = {} | ||||
|  | ||||
|     @abstractmethod | ||||
|   | ||||
| @@ -9,7 +9,9 @@ from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError | ||||
|  | ||||
|  | ||||
| class YouTubeApiVideoProvider(VideoProvider): | ||||
|     name = "YtAPI" | ||||
|     id = "YtAPI" | ||||
|     name = "YouTube API" | ||||
|     description = "Allows communication with YouTube using the YouTube API." | ||||
|     settings = { | ||||
|         "api_key": forms.CharField(label="YouTube API Key:") | ||||
|     } | ||||
| @@ -44,7 +46,7 @@ class YouTubeApiVideoProvider(VideoProvider): | ||||
|  | ||||
|     def fetch_subscription(self, url: str) -> Subscription: | ||||
|         sub = Subscription() | ||||
|         sub.provider_id = self.name | ||||
|         sub.provider_id = self.id | ||||
|  | ||||
|         self.validate_subscription_url(url) | ||||
|         url_parsed = self.__api.parse_url(url) | ||||
|   | ||||
| @@ -12,7 +12,9 @@ from YtManagerApp.scheduler.scheduler import YtsmScheduler | ||||
|  | ||||
| class VideoProviders(containers.DeclarativeContainer): | ||||
|     from YtManagerApp.providers.ytapi_video_provider import YouTubeApiVideoProvider | ||||
|     from YtManagerApp.providers.dummy_video_provider import DummyVideoProvider | ||||
|     ytApiProvider = providers.Factory(YouTubeApiVideoProvider) | ||||
|     dummyProvider = providers.Factory(DummyVideoProvider) | ||||
|  | ||||
|  | ||||
| class Services(containers.DeclarativeContainer): | ||||
| @@ -21,6 +23,9 @@ class Services(containers.DeclarativeContainer): | ||||
|     scheduler = providers.Singleton(YtsmScheduler, appConfig) | ||||
|     youtubeDLManager = providers.Singleton(YoutubeDlManager) | ||||
|     videoManager = providers.Singleton(VideoManager) | ||||
|     videoProviderManager = providers.Singleton(VideoProviderManager, [VideoProviders.ytApiProvider()]) | ||||
|     videoProviderManager = providers.Singleton(VideoProviderManager, [ | ||||
|         VideoProviders.ytApiProvider(), | ||||
|         VideoProviders.dummyProvider(), | ||||
|     ]) | ||||
|     subscriptionManager = providers.Singleton(SubscriptionManager) | ||||
|     downloadManager = providers.Singleton(DownloadManager) | ||||
|   | ||||
| @@ -1,18 +1,42 @@ | ||||
| #main_body { | ||||
|   margin-bottom: 4rem; | ||||
|   margin-top: 0; } | ||||
| :root { | ||||
|   --blue: #007bff; | ||||
|   --indigo: #6610f2; | ||||
|   --purple: #6f42c1; | ||||
|   --pink: #e83e8c; | ||||
|   --red: #dc3545; | ||||
|   --orange: #fd7e14; | ||||
|   --yellow: #ffc107; | ||||
|   --green: #28a745; | ||||
|   --teal: #20c997; | ||||
|   --cyan: #17a2b8; | ||||
|   --white: #fff; | ||||
|   --gray: #6c757d; | ||||
|   --gray-dark: #343a40; | ||||
|   --primary: #007bff; | ||||
|   --secondary: #6c757d; | ||||
|   --success: #28a745; | ||||
|   --info: #17a2b8; | ||||
|   --warning: #ffc107; | ||||
|   --danger: #dc3545; | ||||
|   --light: #f8f9fa; | ||||
|   --dark: #343a40; | ||||
|   --breakpoint-xs: 0; | ||||
|   --breakpoint-sm: 576px; | ||||
|   --breakpoint-md: 768px; | ||||
|   --breakpoint-lg: 992px; | ||||
|   --breakpoint-xl: 1200px; | ||||
|   --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | ||||
|   --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } | ||||
|  | ||||
| #main_footer { | ||||
|   position: fixed; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   height: 2rem; | ||||
|   line-height: 2rem; | ||||
|   padding: 0 1rem; | ||||
|   display: flex; | ||||
|   align-content: center; | ||||
|   font-size: 10pt; } | ||||
| [data-theme="dark"] { | ||||
|   --primary: #007bff; | ||||
|   --secondary: #6c757d; | ||||
|   --success: #28a745; | ||||
|   --info: #17a2b8; | ||||
|   --warning: #ffc107; | ||||
|   --danger: #dc3545; | ||||
|   --light: #343a40; | ||||
|   --dark: #f8f9fa; } | ||||
|  | ||||
| /* Loading animation */ | ||||
| .loading-dual-ring { | ||||
| @@ -26,8 +50,8 @@ | ||||
|     height: 46px; | ||||
|     margin: 1px; | ||||
|     border-radius: 50%; | ||||
|     border: 5px solid #007bff; | ||||
|     border-color: #007bff transparent #007bff transparent; | ||||
|     border: 5px solid var(--primary); | ||||
|     border-color: var(--primary) transparent var(--primary) transparent; | ||||
|     animation: loading-dual-ring 1.2s linear infinite; } | ||||
|  | ||||
| .loading-dual-ring-small { | ||||
| @@ -41,8 +65,8 @@ | ||||
|     height: 23px; | ||||
|     margin: 1px; | ||||
|     border-radius: 50%; | ||||
|     border: 2.5px solid #007bff; | ||||
|     border-color: #007bff transparent #007bff transparent; | ||||
|     border: 2.5px solid var(--primary); | ||||
|     border-color: var(--primary) transparent var(--primary) transparent; | ||||
|     animation: loading-dual-ring 1.2s linear infinite; } | ||||
|  | ||||
| @keyframes loading-dual-ring { | ||||
| @@ -57,6 +81,84 @@ | ||||
|   margin-top: -32px; | ||||
|   margin-left: -32px; } | ||||
|  | ||||
| #hamburger-button { | ||||
|   margin-right: 0.5rem; } | ||||
|   #hamburger-button span { | ||||
|     position: relative; | ||||
|     width: 1.75rem; | ||||
|     height: 0.25rem; | ||||
|     margin-bottom: 0.25rem; | ||||
|     display: block; | ||||
|     background: #cdcdcd; | ||||
|     border-radius: 0.25rem; | ||||
|     transform-origin: 50% 50%; | ||||
|     transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease; } | ||||
|     #hamburger-button span:nth-child(1) { | ||||
|       margin-top: 0.25rem; } | ||||
|     #hamburger-button span:nth-child(2) { | ||||
|       transform-origin: 20% 50%; } | ||||
|   #hamburger-button.hamburger-show span { | ||||
|     opacity: 1; | ||||
|     background: #232323; } | ||||
|     #hamburger-button.hamburger-show span:nth-child(1) { | ||||
|       transform: rotate(-45deg) translate(-0.1875rem, 0.0625rem) scale(0.625, 1); } | ||||
|     #hamburger-button.hamburger-show span:nth-child(2) { | ||||
|       opacity: 0; | ||||
|       transform: rotate(0deg) scale(0, 1); } | ||||
|     #hamburger-button.hamburger-show span:nth-child(3) { | ||||
|       transform: rotate(45deg) translate(-0.1875rem, -0.0625rem) scale(0.625, 1); } | ||||
|  | ||||
| #hamburger { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: -100%; | ||||
|   z-index: 99; | ||||
|   padding-top: 5rem; | ||||
|   width: 20rem; | ||||
|   height: 100%; | ||||
|   display: flex; | ||||
|   background: white; | ||||
|   flex-direction: column; | ||||
|   transition: left 0.5s ease; } | ||||
|   #hamburger.hamburger-show { | ||||
|     left: 0; } | ||||
|   @media (max-width: 768px) { | ||||
|     #hamburger { | ||||
|       width: 100%; } } | ||||
|  | ||||
| #hamburger-footer { | ||||
|   width: 100%; | ||||
|   flex: 0; } | ||||
|   #hamburger-footer a { | ||||
|     margin-left: 0.5rem; | ||||
|     margin-right: 0.5rem; } | ||||
|  | ||||
| #hamburger-content { | ||||
|   overflow-y: auto; | ||||
|   flex: 1; } | ||||
|  | ||||
| #main_navbar { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   width: 100%; | ||||
|   z-index: 100; } | ||||
|  | ||||
| #main_body { | ||||
|   margin-bottom: 4rem; | ||||
|   margin-top: 4rem; } | ||||
|  | ||||
| #main_footer { | ||||
|   position: fixed; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   bottom: 0; | ||||
|   height: 2rem; | ||||
|   line-height: 2rem; | ||||
|   padding: 0 1rem; | ||||
|   display: flex; | ||||
|   align-content: center; | ||||
|   font-size: 10pt; } | ||||
|  | ||||
| .black-overlay { | ||||
|   position: fixed; | ||||
|   /* Sit on top of the page content */ | ||||
| @@ -168,4 +270,20 @@ | ||||
| img.muted { | ||||
|   opacity: .5; } | ||||
|  | ||||
| .provider-wrapper { | ||||
|   padding-left: 5px; | ||||
|   padding-right: 5px; } | ||||
|   .provider-wrapper button { | ||||
|     width: 100%; } | ||||
|  | ||||
| .provider-logo { | ||||
|   display: inline-block; | ||||
|   width: 8rem; | ||||
|   height: 4rem; | ||||
|   object-fit: contain; | ||||
|   margin: .25em auto; | ||||
|   font-size: 3rem; | ||||
|   text-align: center; | ||||
|   line-height: 4rem; } | ||||
|  | ||||
| /*# sourceMappingURL=style.css.map */ | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
| "version": 3, | ||||
| "mappings": "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", | ||||
| "sources": ["style.scss"], | ||||
| "mappings": "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", | ||||
| "sources": ["lib/_theme.scss","lib/_loading.scss","lib/_hamburger.scss","style.scss"], | ||||
| "names": [], | ||||
| "file": "style.css" | ||||
| } | ||||
| @@ -1,8 +1,20 @@ | ||||
| @import "lib/theme"; | ||||
| @import "lib/loading"; | ||||
| @import "lib/hamburger"; | ||||
|  | ||||
| $accent-color: #007bff; | ||||
| $success-color: #28a745; | ||||
|  | ||||
| #main_navbar { | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     width: 100%; | ||||
|     z-index: 100; | ||||
| } | ||||
|  | ||||
| #main_body { | ||||
|     margin-bottom: 4rem; | ||||
|     margin-top: 0; | ||||
|     margin-top: 4rem; | ||||
| } | ||||
|  | ||||
| #main_footer { | ||||
| @@ -18,50 +30,6 @@ $accent-color: #007bff; | ||||
|     font-size: 10pt; | ||||
| } | ||||
|  | ||||
| @mixin loading-dual-ring($scale : 1) { | ||||
|     display: inline-block; | ||||
|     width: $scale * 64px; | ||||
|     height: $scale * 64px; | ||||
|  | ||||
|     &:after { | ||||
|         content: " "; | ||||
|         display: block; | ||||
|         width: $scale * 46px; | ||||
|         height: $scale * 46px; | ||||
|         margin: 1px; | ||||
|         border-radius: 50%; | ||||
|         border: ($scale * 5px) solid $accent-color; | ||||
|         border-color: $accent-color transparent $accent-color transparent; | ||||
|         animation: loading-dual-ring 1.2s linear infinite; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Loading animation */ | ||||
| .loading-dual-ring { | ||||
|     @include loading-dual-ring(1.0); | ||||
| } | ||||
|  | ||||
| .loading-dual-ring-small { | ||||
|     @include loading-dual-ring(0.5); | ||||
| } | ||||
|  | ||||
| @keyframes loading-dual-ring { | ||||
|     0% { | ||||
|         transform: rotate(0deg); | ||||
|     } | ||||
|     100% { | ||||
|         transform: rotate(360deg); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .loading-dual-ring-center-screen { | ||||
|     position: fixed; | ||||
|     top: 50%; | ||||
|     left: 50%; | ||||
|     margin-top: -32px; | ||||
|     margin-left: -32px; | ||||
| } | ||||
|  | ||||
| .black-overlay { | ||||
|     position: fixed; /* Sit on top of the page content */ | ||||
|     display: none; /* Hidden by default */ | ||||
| @@ -213,3 +181,23 @@ $accent-color: #007bff; | ||||
| img.muted { | ||||
|     opacity: .5; | ||||
| } | ||||
|  | ||||
| .provider-wrapper { | ||||
|     padding-left: 5px; | ||||
|     padding-right: 5px; | ||||
|     button { | ||||
|         width: 100%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .provider-logo { | ||||
|     display: inline-block; | ||||
|     width: 8rem; | ||||
|     height: 4rem; | ||||
|     object-fit: contain; | ||||
|     margin: .25em auto; | ||||
|  | ||||
|     font-size: 3rem; | ||||
|     text-align: center; | ||||
|     line-height: 4rem; | ||||
| } | ||||
										
											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-group mr-2" role="group"> | ||||
|                     <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> | ||||
|                     </button> | ||||
|                     <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> | ||||
|                     </button> | ||||
|                     <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> | ||||
|                     </button> | ||||
|                 </div> | ||||
|   | ||||
| @@ -6,175 +6,6 @@ function zeroFill(number, width) { | ||||
|     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() { | ||||
|     $.post("{% url 'ajax_action_sync_now' %}", { | ||||
|         csrfmiddlewaretoken: '{{ csrf_token }}' | ||||
|   | ||||
| @@ -175,84 +175,6 @@ function videos_Submit(e) | ||||
|         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 | ||||
| /// | ||||
| @@ -263,22 +185,22 @@ $(document).ready(function () | ||||
|  | ||||
|     tree_Initialize(); | ||||
|  | ||||
|     // Subscription toolbar | ||||
|     $("#btn_create_sub").on("click", function () { | ||||
|         let modal = new AjaxModal("{% url 'modal_create_subscription' %}"); | ||||
|         modal.setSubmitCallback(tree_Refresh); | ||||
|         modal.loadAndShow(); | ||||
|     }); | ||||
|     $("#btn_create_folder").on("click", function () { | ||||
|         let modal = new AjaxModal("{% url 'modal_create_folder' %}"); | ||||
|         modal.setSubmitCallback(tree_Refresh); | ||||
|         modal.loadAndShow(); | ||||
|     }); | ||||
|     $("#btn_import").on("click", function () { | ||||
|         let modal = new AjaxModal("{% url 'modal_import_subscriptions' %}"); | ||||
|         modal.setSubmitCallback(tree_Refresh); | ||||
|         modal.loadAndShow(); | ||||
|     }); | ||||
|     // // Subscription toolbar | ||||
|     // $("#btn_create_sub").on("click", function () { | ||||
|     //     let modal = new AjaxModal("{% url 'modal_create_subscription' %}"); | ||||
|     //     modal.setSubmitCallback(tree_Refresh); | ||||
|     //     modal.loadAndShow(); | ||||
|     // }); | ||||
|     // $("#btn_create_folder").on("click", function () { | ||||
|     //     let modal = new AjaxModal("{% url 'modal_create_folder' %}"); | ||||
|     //     modal.setSubmitCallback(tree_Refresh); | ||||
|     //     modal.loadAndShow(); | ||||
|     // }); | ||||
|     // $("#btn_import").on("click", function () { | ||||
|     //     let modal = new AjaxModal("{% url 'modal_import_subscriptions' %}"); | ||||
|     //     modal.setSubmitCallback(tree_Refresh); | ||||
|     //     modal.loadAndShow(); | ||||
|     // }); | ||||
|     $("#btn_edit_node").on("click", treeNode_Edit); | ||||
|     $("#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); | ||||
|  | ||||
|     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/typicons/typicons.min.css' %}" /> | ||||
|         <link rel="stylesheet" href="{% static 'YtManagerApp/css/style.css' %}"> | ||||
|  | ||||
|         {% block stylesheets %} | ||||
|         {% endblock %} | ||||
|     </head> | ||||
|     <body> | ||||
|          | ||||
|         <nav 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> | ||||
|         <nav id="main_navbar" class="navbar navbar-expand-lg navbar-light bg-light"> | ||||
|  | ||||
|             <div class="collapse navbar-collapse" id="navbarSupportedContent"> | ||||
|                 <ul class="navbar-nav ml-auto"> | ||||
|                     {% if request.user.is_authenticated %} | ||||
|                         <li class="nav-item dropdown"> | ||||
|                             <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" | ||||
|                                data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                                 Welcome, | ||||
|                                 {% if request.user.first_name %} | ||||
|                                     {{ request.user.first_name }} | ||||
|                                 {% else %} | ||||
|                                     {{ request.user.username }} | ||||
|                                 {% endif %} | ||||
|                             </a> | ||||
|                             <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userDropdown"> | ||||
|                                 <a class="dropdown-item" href="{% url 'settings' %}">Settings</a> | ||||
|                                 {% if request.user.is_superuser %} | ||||
|                                     <a class="dropdown-item" href="{% url 'admin_settings' %}">Admin settings</a> | ||||
|                                 {% endif %} | ||||
|                                 <div class="dropdown-divider"></div> | ||||
|                                 <a class="dropdown-item" href="{% url 'logout' %}">Log out</a> | ||||
|                             </div> | ||||
|                         </li> | ||||
|                     {% else %} | ||||
|             {% if request.user.is_authenticated %} | ||||
|                 <button id="hamburger-button" | ||||
|                         type="button" | ||||
|                         class="btn btn-light"> | ||||
|                     <span></span> | ||||
|                     <span></span> | ||||
|                     <span></span> | ||||
|                 </button> | ||||
|             {% endif %} | ||||
|  | ||||
|             <a class="navbar-brand" href="{% url 'home' %}">YouTube Manager</a> | ||||
|  | ||||
|             {% block navbar-content %} | ||||
|             {% endblock %} | ||||
|  | ||||
|             <ul class="navbar-nav ml-auto"> | ||||
|                 {% if not request.user.is_authenticated %} | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="{% url 'login' %}">Login</a> | ||||
|                     </li> | ||||
|                     {% if global_preferences.general__allow_registrations %} | ||||
|                         <li class="nav-item"> | ||||
|                             <a class="nav-link" href="{% url 'login' %}">Login</a> | ||||
|                             <a class="nav-link" href="{% url 'register' %}">Register</a> | ||||
|                         </li> | ||||
|                         {% if global_preferences.general__allow_registrations %} | ||||
|                             <li class="nav-item"> | ||||
|                                 <a class="nav-link" href="{% url 'register' %}">Register</a> | ||||
|                             </li> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                 </ul> | ||||
|             </div> | ||||
|                 {% endif %} | ||||
|             </ul> | ||||
|  | ||||
|         </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"> | ||||
|             {% block body %} | ||||
|             {% endblock %} | ||||
| @@ -77,7 +89,7 @@ | ||||
|             <div class="btn-group"> | ||||
|                 <button id="btn_toggle_job_panel" | ||||
|                         class="btn btn-sm btn-light dropdown-toggle" | ||||
|                         title="Show/hide details" | ||||
|                         title="Show/hide job details" | ||||
|                         data-toggle="dropdown" | ||||
|                         aria-haspopup="true" aria-expanded="false"> | ||||
|                     <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/popper/popper.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> | ||||
|             {% include 'YtManagerApp/js/common.js' %} | ||||
|         </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.urls import path | ||||
|  | ||||
| from YtManagerApp.views.settings.providers_view import ProvidersView | ||||
| from .views import first_time | ||||
| from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \ | ||||
|     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, \ | ||||
|     CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal | ||||
| 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 | ||||
|  | ||||
| urlpatterns = [ | ||||
| @@ -63,6 +64,7 @@ urlpatterns = [ | ||||
|     # Pages | ||||
|     path('', index, name='home'), | ||||
|     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('video/<int:pk>/', VideoDetailView.as_view(), name='video'), | ||||
|     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 | ||||
		Reference in New Issue
	
	Block a user