diff --git a/app/YtManagerApp/management/video_provider_manager.py b/app/YtManagerApp/management/video_provider_manager.py index 6db28ae..e411105 100644 --- a/app/YtManagerApp/management/video_provider_manager.py +++ b/app/YtManagerApp/management/video_provider_manager.py @@ -1,12 +1,15 @@ import logging -from typing import List, Dict, Union +from typing import List, Dict, Union, Iterable from YtManagerApp.models import VideoProviderConfig, Video, Subscription from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError import json +from collections import namedtuple log = logging.getLogger("VideoProviderManager") +VideoProviderInfo = namedtuple('VideoProviderInfo', ['id', 'name', 'is_configured', 'description']) + class VideoProviderManager(object): def __init__(self, registered_providers: List[VideoProvider]): @@ -23,18 +26,18 @@ class VideoProviderManager(object): :param provider: Video provider """ # avoid duplicates - if provider.name in self._registered_providers: - log.error(f"Duplicate video provider {provider.name}") + if provider.id in self._registered_providers: + log.error(f"Duplicate video provider {provider.id}") return # register - self._registered_providers[provider.name] = provider - log.info(f"Registered video provider {provider.name}") + self._registered_providers[provider.id] = provider + log.info(f"Registered video provider {provider.id}") # load configuration (if any) - if provider.name in self._pending_configs: - self._configure(provider, self._pending_configs[provider.name]) - del self._pending_configs[provider.name] + if provider.id in self._pending_configs: + self._configure(provider, self._pending_configs[provider.id]) + del self._pending_configs[provider.id] def _load(self) -> None: # Loads configuration from database @@ -53,8 +56,8 @@ class VideoProviderManager(object): def _configure(self, provider, config): settings = json.loads(config.settings) provider.configure(settings) - log.info(f"Configured video provider {provider.name}") - self._configured_providers[provider.name] = provider + log.info(f"Configured video provider {provider.id}") + self._configured_providers[provider.id] = provider def get(self, item: Union[str, Subscription, Video]): """ @@ -100,3 +103,14 @@ class VideoProviderManager(object): pass raise InvalidURLError("The given URL is not valid for any of the supported sites!") + + def get_available_providers(self) -> Iterable[VideoProviderInfo]: + """ + Gets a list of available providers and some basic information about them. + :return: List of dictionary entries + """ + for key, provider in self._registered_providers.items(): + yield VideoProviderInfo(id=key, + name=provider.name, + description=provider.description, + is_configured=(key in self._configured_providers)) diff --git a/app/YtManagerApp/providers/dummy_video_provider.py b/app/YtManagerApp/providers/dummy_video_provider.py new file mode 100644 index 0000000..8fc1c12 --- /dev/null +++ b/app/YtManagerApp/providers/dummy_video_provider.py @@ -0,0 +1,47 @@ +from typing import Dict, Optional, Any, Iterable, List + +from django import forms +from external.pytaw.pytaw import youtube as yt +from external.pytaw.pytaw.utils import iterate_chunks + +from YtManagerApp.models import Subscription, Video +from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError, ProviderValidationError + + +class DummyVideoProvider(VideoProvider): + id = "Dummy" + name = "Dummy Videos" + description = "Won't really do anything, it's here just for testing." + settings = { + "api_key": forms.CharField(label="Dummy API Key"), + "number_of_something": forms.IntegerField(label="Number of stuff") + } + + def configure(self, configuration: Dict[str, Any]) -> None: + print(configuration) + + def validate_configuration(self, configuration: Dict[str, Any]): + print("Validating...") + if configuration["number_of_something"] >= 10: + raise ProviderValidationError( + field_messages={'number_of_something': "Number too large, try something smaller!"}) + pass + + def get_subscription_url(self, subscription: Subscription): + return f"https://dummy/playlist/{subscription.playlist_id}" + + def validate_subscription_url(self, url: str) -> None: + if not url.startswith('https://dummy/'): + raise InvalidURLError("URL not valid") + + def fetch_subscription(self, url: str) -> Subscription: + raise ValueError('No such subscription (note: dummy plugin, nothing will work)!') + + def get_video_url(self, video: Video) -> str: + return f"https://dummy/video/{video.video_id}" + + def fetch_videos(self, subscription: Subscription) -> Iterable[Video]: + return [] + + def update_videos(self, videos: List[Video], update_metadata=False, update_statistics=False) -> None: + pass diff --git a/app/YtManagerApp/providers/video_provider.py b/app/YtManagerApp/providers/video_provider.py index 63453aa..f377fe4 100644 --- a/app/YtManagerApp/providers/video_provider.py +++ b/app/YtManagerApp/providers/video_provider.py @@ -6,7 +6,7 @@ from django.forms import Field from YtManagerApp.models import Subscription, Video -class ConfigurationValidationError(ValueError): +class ProviderValidationError(ValueError): """ Exception type thrown when validating configurations. """ @@ -27,7 +27,21 @@ class InvalidURLError(ValueError): class VideoProvider(ABC): + """ + Identifier + """ + id: str = "" + """ + Display name, shown to users + """ name: str = "" + """ + Description, shown to users + """ + description: str = "" + """ + Dictionary containing fields necessary for configuring + """ settings: Dict[str, Field] = {} @abstractmethod diff --git a/app/YtManagerApp/providers/ytapi_video_provider.py b/app/YtManagerApp/providers/ytapi_video_provider.py index 98bb752..538b394 100644 --- a/app/YtManagerApp/providers/ytapi_video_provider.py +++ b/app/YtManagerApp/providers/ytapi_video_provider.py @@ -9,7 +9,9 @@ from YtManagerApp.providers.video_provider import VideoProvider, InvalidURLError class YouTubeApiVideoProvider(VideoProvider): - name = "YtAPI" + id = "YtAPI" + name = "YouTube API" + description = "Allows communication with YouTube using the YouTube API." settings = { "api_key": forms.CharField(label="YouTube API Key:") } @@ -44,7 +46,7 @@ class YouTubeApiVideoProvider(VideoProvider): def fetch_subscription(self, url: str) -> Subscription: sub = Subscription() - sub.provider_id = self.name + sub.provider_id = self.id self.validate_subscription_url(url) url_parsed = self.__api.parse_url(url) diff --git a/app/YtManagerApp/services.py b/app/YtManagerApp/services.py index 7393580..6cb5221 100644 --- a/app/YtManagerApp/services.py +++ b/app/YtManagerApp/services.py @@ -12,7 +12,9 @@ from YtManagerApp.scheduler.scheduler import YtsmScheduler class VideoProviders(containers.DeclarativeContainer): from YtManagerApp.providers.ytapi_video_provider import YouTubeApiVideoProvider + from YtManagerApp.providers.dummy_video_provider import DummyVideoProvider ytApiProvider = providers.Factory(YouTubeApiVideoProvider) + dummyProvider = providers.Factory(DummyVideoProvider) class Services(containers.DeclarativeContainer): @@ -21,6 +23,9 @@ class Services(containers.DeclarativeContainer): scheduler = providers.Singleton(YtsmScheduler, appConfig) youtubeDLManager = providers.Singleton(YoutubeDlManager) videoManager = providers.Singleton(VideoManager) - videoProviderManager = providers.Singleton(VideoProviderManager, [VideoProviders.ytApiProvider()]) + videoProviderManager = providers.Singleton(VideoProviderManager, [ + VideoProviders.ytApiProvider(), + VideoProviders.dummyProvider(), + ]) subscriptionManager = providers.Singleton(SubscriptionManager) downloadManager = providers.Singleton(DownloadManager) diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.css b/app/YtManagerApp/static/YtManagerApp/css/style.css index 58fcaa6..a473add 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.css +++ b/app/YtManagerApp/static/YtManagerApp/css/style.css @@ -1,18 +1,42 @@ -#main_body { - margin-bottom: 4rem; - margin-top: 0; } +:root { + --blue: #007bff; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #dc3545; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #28a745; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #f8f9fa; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } -#main_footer { - position: fixed; - left: 0; - right: 0; - bottom: 0; - height: 2rem; - line-height: 2rem; - padding: 0 1rem; - display: flex; - align-content: center; - font-size: 10pt; } +[data-theme="dark"] { + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #343a40; + --dark: #f8f9fa; } /* Loading animation */ .loading-dual-ring { @@ -26,8 +50,8 @@ height: 46px; margin: 1px; border-radius: 50%; - border: 5px solid #007bff; - border-color: #007bff transparent #007bff transparent; + border: 5px solid var(--primary); + border-color: var(--primary) transparent var(--primary) transparent; animation: loading-dual-ring 1.2s linear infinite; } .loading-dual-ring-small { @@ -41,8 +65,8 @@ height: 23px; margin: 1px; border-radius: 50%; - border: 2.5px solid #007bff; - border-color: #007bff transparent #007bff transparent; + border: 2.5px solid var(--primary); + border-color: var(--primary) transparent var(--primary) transparent; animation: loading-dual-ring 1.2s linear infinite; } @keyframes loading-dual-ring { @@ -57,6 +81,84 @@ margin-top: -32px; margin-left: -32px; } +#hamburger-button { + margin-right: 0.5rem; } + #hamburger-button span { + position: relative; + width: 1.75rem; + height: 0.25rem; + margin-bottom: 0.25rem; + display: block; + background: #cdcdcd; + border-radius: 0.25rem; + transform-origin: 50% 50%; + transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease; } + #hamburger-button span:nth-child(1) { + margin-top: 0.25rem; } + #hamburger-button span:nth-child(2) { + transform-origin: 20% 50%; } + #hamburger-button.hamburger-show span { + opacity: 1; + background: #232323; } + #hamburger-button.hamburger-show span:nth-child(1) { + transform: rotate(-45deg) translate(-0.1875rem, 0.0625rem) scale(0.625, 1); } + #hamburger-button.hamburger-show span:nth-child(2) { + opacity: 0; + transform: rotate(0deg) scale(0, 1); } + #hamburger-button.hamburger-show span:nth-child(3) { + transform: rotate(45deg) translate(-0.1875rem, -0.0625rem) scale(0.625, 1); } + +#hamburger { + position: fixed; + top: 0; + left: -100%; + z-index: 99; + padding-top: 5rem; + width: 20rem; + height: 100%; + display: flex; + background: white; + flex-direction: column; + transition: left 0.5s ease; } + #hamburger.hamburger-show { + left: 0; } + @media (max-width: 768px) { + #hamburger { + width: 100%; } } + +#hamburger-footer { + width: 100%; + flex: 0; } + #hamburger-footer a { + margin-left: 0.5rem; + margin-right: 0.5rem; } + +#hamburger-content { + overflow-y: auto; + flex: 1; } + +#main_navbar { + position: fixed; + top: 0; + width: 100%; + z-index: 100; } + +#main_body { + margin-bottom: 4rem; + margin-top: 4rem; } + +#main_footer { + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: 2rem; + line-height: 2rem; + padding: 0 1rem; + display: flex; + align-content: center; + font-size: 10pt; } + .black-overlay { position: fixed; /* Sit on top of the page content */ @@ -168,4 +270,20 @@ img.muted { opacity: .5; } +.provider-wrapper { + padding-left: 5px; + padding-right: 5px; } + .provider-wrapper button { + width: 100%; } + +.provider-logo { + display: inline-block; + width: 8rem; + height: 4rem; + object-fit: contain; + margin: .25em auto; + font-size: 3rem; + text-align: center; + line-height: 4rem; } + /*# sourceMappingURL=style.css.map */ diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.css.map b/app/YtManagerApp/static/YtManagerApp/css/style.css.map index 1857bfa..6d30ccb 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.css.map +++ b/app/YtManagerApp/static/YtManagerApp/css/style.css.map @@ -1,7 +1,7 @@ { "version": 3, -"mappings": "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", -"sources": ["style.scss"], +"mappings": "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", +"sources": ["lib/_theme.scss","lib/_loading.scss","lib/_hamburger.scss","style.scss"], "names": [], "file": "style.css" } \ No newline at end of file diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.scss b/app/YtManagerApp/static/YtManagerApp/css/style.scss index 7d020ff..430b197 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.scss +++ b/app/YtManagerApp/static/YtManagerApp/css/style.scss @@ -1,8 +1,20 @@ +@import "lib/theme"; +@import "lib/loading"; +@import "lib/hamburger"; + $accent-color: #007bff; +$success-color: #28a745; + +#main_navbar { + position: fixed; + top: 0; + width: 100%; + z-index: 100; +} #main_body { margin-bottom: 4rem; - margin-top: 0; + margin-top: 4rem; } #main_footer { @@ -18,50 +30,6 @@ $accent-color: #007bff; font-size: 10pt; } -@mixin loading-dual-ring($scale : 1) { - display: inline-block; - width: $scale * 64px; - height: $scale * 64px; - - &:after { - content: " "; - display: block; - width: $scale * 46px; - height: $scale * 46px; - margin: 1px; - border-radius: 50%; - border: ($scale * 5px) solid $accent-color; - border-color: $accent-color transparent $accent-color transparent; - animation: loading-dual-ring 1.2s linear infinite; - } -} - -/* Loading animation */ -.loading-dual-ring { - @include loading-dual-ring(1.0); -} - -.loading-dual-ring-small { - @include loading-dual-ring(0.5); -} - -@keyframes loading-dual-ring { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.loading-dual-ring-center-screen { - position: fixed; - top: 50%; - left: 50%; - margin-top: -32px; - margin-left: -32px; -} - .black-overlay { position: fixed; /* Sit on top of the page content */ display: none; /* Hidden by default */ @@ -212,4 +180,24 @@ $accent-color: #007bff; img.muted { opacity: .5; +} + +.provider-wrapper { + padding-left: 5px; + padding-right: 5px; + button { + width: 100%; + } +} + +.provider-logo { + display: inline-block; + width: 8rem; + height: 4rem; + object-fit: contain; + margin: .25em auto; + + font-size: 3rem; + text-align: center; + line-height: 4rem; } \ No newline at end of file diff --git a/app/YtManagerApp/static/YtManagerApp/img/video_providers/Dummy.png b/app/YtManagerApp/static/YtManagerApp/img/video_providers/Dummy.png new file mode 100644 index 0000000..01abd6c Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/video_providers/Dummy.png differ diff --git a/app/YtManagerApp/static/YtManagerApp/img/video_providers/YtAPI.png b/app/YtManagerApp/static/YtManagerApp/img/video_providers/YtAPI.png new file mode 100644 index 0000000..165e04b Binary files /dev/null and b/app/YtManagerApp/static/YtManagerApp/img/video_providers/YtAPI.png differ diff --git a/app/YtManagerApp/static/YtManagerApp/js/common.js b/app/YtManagerApp/static/YtManagerApp/js/common.js new file mode 100644 index 0000000..616c9a6 --- /dev/null +++ b/app/YtManagerApp/static/YtManagerApp/js/common.js @@ -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(); + }); +}); + diff --git a/app/YtManagerApp/static/YtManagerApp/js/components/AjaxModal.js b/app/YtManagerApp/static/YtManagerApp/js/components/AjaxModal.js new file mode 100644 index 0000000..88f2c4b --- /dev/null +++ b/app/YtManagerApp/static/YtManagerApp/js/components/AjaxModal.js @@ -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('
An error occurred while displaying the dialog!
'); + } + + _submit(e) { + let pThis = this; + let url = this.form.attr('action'); + let ajax_settings = { + url: url, + }; + + if (this.form.attr('enctype') === 'multipart/form-data') { + ajax_settings.data = new FormData(this.form[0]); + ajax_settings.contentType = false; + ajax_settings.processData = false; + ajax_settings.cache = false; + } + else { + ajax_settings.data = this.form.serialize(); + } + + $.post(ajax_settings) + .done(function(result) { + pThis._submitDone(result); + }) + .fail(function() { + pThis._submitFailed(); + }) + .always(function() { + pThis.modalLoadingRing.fadeOut(100); + pThis.wrapper.find(":input").prop("disabled", false); + }); + + this.modalLoadingRing.fadeIn(200); + this.wrapper.find(":input").prop("disabled", true); + + e.preventDefault(); + } + + _submitDone(result) { + // Clear old errors first + this.form.find('.modal-field-error').remove(); + + if (!result.hasOwnProperty('success')) { + this._submitInvalidResponse(); + return; + } + + if (result.success) { + this._hideModal(); + if (this.submitCallback != null) + this.submitCallback(); + } + else { + if (!result.hasOwnProperty('errors')) { + this._submitInvalidResponse(); + return; + } + + for (let field in result.errors) + { + let errorsArray = result.errors[field]; + let errorsConcat = "
'; + + if (field === '__all__') + this.form.find('.modal-body').append(errorsConcat); + else + this.form.find(`[name='${field}']`).after(errorsConcat); + } + + let errorsHtml = ''; + + let err = this.modal.find('#__modal_error'); + if (err.length) { + err.html('An error occurred'); + } + else { + this.modal.find('.modal-body').append(errorsHtml) + } + } + } + + _submitFailed() { + // Clear old errors first + this.form.find('.modal-field-error').remove(); + this.form.find('.modal-body') + .append(``); + } + + _submitInvalidResponse() { + // Clear old errors first + this.form.find('.modal-field-error').remove(); + this.form.find('.modal-body') + .append(``); + } + + loadAndShow() + { + let pThis = this; + this._showLoading(); + + $.get(this.url) + .done(function (result) { + pThis._load(result); + pThis._showModal(); + }) + .fail(function () { + pThis._loadFailed(); + }) + .always(function() { + pThis._hideLoading(); + }); + } +} \ No newline at end of file diff --git a/app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js b/app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js new file mode 100644 index 0000000..082e47c --- /dev/null +++ b/app/YtManagerApp/static/YtManagerApp/js/components/JobPanel.js @@ -0,0 +1,213 @@ +export class JobEntry +{ + id = ""; + description = ""; + message = ""; + progress = 0; + + dom = null; + + constructor(job) + { + this.id = job.id; + this.description = job.description; + this.message = job.message; + this.progress = job.progress; + } + + createDom(template, parent) + { + this.dom = template.clone(); + this.dom.attr('id', `job_${this.id}`); + this.dom.addClass('job_entry'); + this.dom.removeClass('collapse'); + + this.updateDom(); + + this.dom.appendTo(parent); + } + + update(job) + { + if (job !== null) { + this.description = job.description; + this.message = job.message; + this.progress = job.progress; + } + + this.updateDom(); + } + + updateDom() + { + if (this.dom === null) { + return; + } + this.dom.find('#job_panel_item_title').text(this.description); + this.dom.find('#job_panel_item_subtitle').text(this.message); + + let entryPercent = 100 * this.progress; + let jobEntryProgress = this.dom.find('#job_panel_item_progress'); + jobEntryProgress.width(entryPercent + '%'); + jobEntryProgress.text(`${entryPercent.toFixed(0)}%`); + } + + deleteDom() + { + if (this.dom === null) { + return; + } + this.dom.remove(); + this.dom = null; + } +} + +export class JobPanel +{ + static QUERY_INTERVAL = 1500; + + statusBar_Progress = null; + panel = null; + panel_Title = null; + panel_TitleNoJobs = null; + panel_JobTemplate = null; + + jobs = []; + + constructor() + { + this.statusBar_Progress = $('#status-progress'); + this.panel = $('#job_panel'); + this.panel_Title = this.panel.find('#job_panel_title'); + this.panel_TitleNoJobs = this.panel.find('#job_panel_no_jobs_title'); + this.panel_JobTemplate = this.panel.find('#job_panel_item_template'); + } + + update() + { + let pThis = this; + + $.get(window.ytsmContext.url_ajax_get_running_jobs) + .done(function(data, textStatus, jqXHR) { + if (jqXHR.getResponseHeader('content-type') === "application/json") { + pThis._updateInternal(data); + } + else { + pThis._clear(); + } + }); + } + + _updateInternal(data) + { + this._updateJobs(data); + this._updateStatusBar(); + this._updateProgressBar(); + this._updateTitle(); + + $('#btn_toggle_job_panel').dropdown('update'); + } + + _updateJobs(data) + { + let keep = []; + + for (let srvJob of data) + { + let found = false; + + // Find existing jobs + for (let job of this.jobs) { + if (job.id === srvJob.id) { + job.update(srvJob); + found = true; + keep.push(job.id); + } + } + + // New job + if (!found) { + let job = new JobEntry(srvJob); + job.createDom(this.panel_JobTemplate, this.panel); + this.jobs.push(job); + keep.push(job.id); + } + } + + // Delete old jobs + for (let i = 0; i < this.jobs.length; i++) { + if (keep.indexOf(this.jobs[i].id) < 0) { + this.jobs[i].deleteDom(); + this.jobs.splice(i--, 1); + } + } + } + + _clear() + { + // Delete old jobs + for (let i = 0; i < this.jobs.length; i++) { + this.jobs[i].deleteDom(); + } + this.jobs = []; + } + + _updateTitle() + { + if (this.jobs.length === 0) { + this.panel_Title.addClass('collapse'); + this.panel_TitleNoJobs.removeClass('collapse'); + } + else { + this.panel_Title.removeClass('collapse'); + this.panel_TitleNoJobs.addClass('collapse'); + } + } + + _updateStatusBar() + { + let text = ""; + + if (this.jobs.length === 1) { + text = `${this.jobs[0].description} | ${this.jobs[0].message}`; + } + else if (this.jobs.length > 1) { + text = `Running ${this.jobs.length} jobs...`; + } + $('#status-message').text(text); + } + + _updateProgressBar() + { + if (this.jobs.length > 0) { + // Make visible + this.statusBar_Progress.removeClass('invisible'); + + // Calculate progress + let combinedProgress = 0; + for (let job of this.jobs) { + combinedProgress += job.progress; + } + + let percent = 100 * combinedProgress / this.jobs.length; + + let bar = this.statusBar_Progress.find('.progress-bar'); + bar.width(percent + '%'); + bar.text(`${percent.toFixed(0)}%`); + } + else { + // hide + this.statusBar_Progress.addClass('invisible'); + } + } + + enable() + { + this.update(); + + let pThis = this; + setInterval(function() { + pThis.update(); + }, JobPanel.QUERY_INTERVAL); + } +} \ No newline at end of file diff --git a/app/YtManagerApp/templates/YtManagerApp/index.html b/app/YtManagerApp/templates/YtManagerApp/index.html index 46ef6d9..c18de29 100644 --- a/app/YtManagerApp/templates/YtManagerApp/index.html +++ b/app/YtManagerApp/templates/YtManagerApp/index.html @@ -33,15 +33,21 @@