From 4ef93df541ad8385b7cd540cc1aed9676cfc2cda Mon Sep 17 00:00:00 2001 From: Anoncer Date: Mon, 6 May 2024 11:31:40 +0200 Subject: [PATCH 1/3] Added to Api get UserRates (show) --- javascript/modules/ShikiAPI.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/javascript/modules/ShikiAPI.js b/javascript/modules/ShikiAPI.js index 75e8167..c66d828 100644 --- a/javascript/modules/ShikiAPI.js +++ b/javascript/modules/ShikiAPI.js @@ -209,6 +209,14 @@ export const UserRates = { const response = await request.fetch(); event(response); return response; + }, + + GET: async () => { + request.setMethod("GET"); + request.setHeaders(Headers.bearer()); + const response = await request.fetch(); + event(response); + return response; } } }, From a1a5a686ac6769c2986e410e863d6abc53bb7a21 Mon Sep 17 00:00:00 2001 From: Anoncer Date: Tue, 7 May 2024 18:55:42 +0200 Subject: [PATCH 2/3] Updatet Window (Download Anime) Added new Parameter autoset episode watched when downloaded. --- README.md | 1 + javascript/auto/download_a.js | 203 +++++++ javascript/engine/event_handler.js | 27 +- javascript/pages/settings.js | 8 +- javascript/pages/watch/mod_download.js | 753 ++++++++++++++++--------- javascript/pages/watch/mod_resource.js | 33 +- javascript/parametrs.js | 3 +- style/css/watch.css | 241 +++++--- style/watch/_download.scss | 295 ++++++---- watch.html | 49 +- 10 files changed, 1130 insertions(+), 483 deletions(-) create mode 100644 javascript/auto/download_a.js diff --git a/README.md b/README.md index 9b5b826..573eb0d 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,4 @@ ___ + \ No newline at end of file diff --git a/javascript/auto/download_a.js b/javascript/auto/download_a.js new file mode 100644 index 0000000..3ab0460 --- /dev/null +++ b/javascript/auto/download_a.js @@ -0,0 +1,203 @@ +import { Sleep } from "../modules/funcitons.js"; +import { ShowInfo } from "../modules/Popup.js"; +import { UserRates } from "../modules/ShikiAPI.js"; +import { User } from "../modules/ShikiUSR.js"; + +const SYNC_ENABLE = $PARAMETERS.anime.syncdata; +const AUTO_ENABLE = $PARAMETERS.download.dautoset; + +(async () => { + const key = "download-a"; + + /** + * @type {[{id_ur: number, episodes: number, downloaded: [{episode: number, date:string}], duration: number}] | []} + */ + const data = JSON.parse(localStorage.getItem(key)) || []; + + if (data.length === 0 || !User.authorized || !AUTO_ENABLE) + return; + + let set = []; + + const a = new Date(); + + for (let i = 0; i < data.length; i++) { + const element = data[i]; + let allowed = true; + for (let e = 0; e < element.downloaded.length; e++) { + const episode = element.downloaded[e]; + let b = new Date(episode.date); + + b.setMinutes(b.getMinutes() + element.duration); + b.setTime(b.getTime() + (12 * 60 * 60 * 1000)); + + if (a.getTime() > b.getTime() && allowed) { + set.push({ id: element.id_ur, episode: episode.episode }); + } else { + allowed = false; + set = set.filter(item => item.id !== element.id_ur); + } + } + } + + set = Filter(set); + + let ids = []; + + for (let i = 0; i < set.length; i++) { + const element = set[i]; + + await Sleep(1000); + const completed = await Update(element.id, element.episode, element.count); + + if (completed) + ids.push(element.id); + } + + for (let i = 0; i < ids.length; i++) { + const element = ids[i]; + const index = data.findIndex(x => x.id_ur == element); + if (index != -1) { + data.splice(index, 1); + } + } + + if (ids.length != 0) + ShowInfo(`Обновлено ${ids.length} загруженных аниме`, key); + + if (data.length === 0) + return localStorage.removeItem(key); + localStorage.setItem(key, JSON.stringify(data)); +})(); + +function Update(id, episode, count) { + const status = ["watching", "rewatching", "planned", "dropped"] + return new Promise((resolve) => { + UserRates.show(id, async (response) => { + if (response.failed && response.status == 429) { + await Sleep(1000); + return resolve(Update(id)); + } + + if (response.failed) { + return resolve(false); + } + + if (response.episodes < episode && status.includes(response.status) && (response.episodes + count + 1) === episode) { + return resolve((await SetWatched(episode, response))); + } else { + return resolve(true); + } + }).GET(); + }) +} + + +/** + * + * @param {*} data + * @returns {[{ id: number, episode: number, count: number }]} + */ +function Filter(data) { + // Создаем объект для хранения эпизодов по каждому id + const ids = {}; + + // Разбиваем данные по id + for (const item of data) { + if (!ids[item.id]) { + ids[item.id] = { episodes: [] }; + } + ids[item.id].episodes.push(item.episode); + } + + const retData = []; + + // Проходимся по каждому id и находим максимальный эпизод + for (const id in ids) { + if (Object.hasOwnProperty.call(ids, id)) { + const episodes = ids[id].episodes; + episodes.sort((a, b) => a - b); + + // Находим максимальный эпизод и считаем количество эпизодов после него + let maxEpisode = episodes[0]; + let count = 0; + for (let i = 0; i < episodes.length; i++) { + if (episodes[i] >= maxEpisode && episodes[i] - maxEpisode === 1) { + maxEpisode = episodes[i]; + count++; + } + } + + // Добавляем данные в результирующий массив + retData.push({ id: parseInt(id), episode: maxEpisode, count }); + } + } + + return retData; +} + +/** + * Сохраняет данные аниме и обновляет комментарий + * @param {number} e - Эпизод аниме + * @param {Object} user_rate - данные прользователя об аниме + */ +function SetWatched(e, user_rate) { + return new Promise((resolve) => { + let body = { "user_rate": { "episodes": e } }; + + if (user_rate.status == "planned" || user_rate.status == "dropped") + body.user_rate["status"] = "watching"; + + if (localStorage.getItem(user_rate.target_id)) { + /**@type {{kodik_episode:number, kodik_dub:number, date_update:number} || null} */ + const data = JSON.parse(localStorage.getItem(user_rate.target_id)) || null; + + if (data != null) { + data.kodik_episode = e; + data.date_update = new Date(); + + localStorage.setItem(user_rate.target_id, JSON.stringify(data)); + + if (SYNC_ENABLE) { + const regex = /\[tunime-sync:(\d+):(\d+):"(.+?)"]/; + let match = ""; + + if (user_rate.text) { + match = user_rate.text.match(regex); + } + + if (match) { + user_rate.text = user_rate.text.replace(match[0], ''); + } + + if (user_rate.text) { + user_rate.text = user_rate.text.trim(); + } else { + user_rate.text = ""; + } + + user_rate.text += `\r\n[tunime-sync:${data.kodik_episode}:${data.kodik_dub}:${JSON.stringify(data.date_update)}]`; + body.user_rate["text"] = user_rate.text; + } + } + } + return resolve(Fetch(user_rate.id, body)); + }); +} + +function Fetch(id, body) { + return new Promise((resolve) => { + UserRates.show(id, async (response) => { + if (response.failed && response.status == 429) { + await Sleep(1000); + return resolve(Fetch(id, body)); + } + + if (response.failed) { + return resolve(false); + } + + return resolve(true); + }).PATCH(body); + }); +} \ No newline at end of file diff --git a/javascript/engine/event_handler.js b/javascript/engine/event_handler.js index 8dffa7e..366c813 100644 --- a/javascript/engine/event_handler.js +++ b/javascript/engine/event_handler.js @@ -38,11 +38,34 @@ let app_installed_device = { if (key == 'version') continue; - if(events_handler_list[key].completed == false && events_handler_list[key].target == app_current_page){ + if (events_handler_list[key].completed == false && events_handler_list[key].target == app_current_page) { CallEventHandler(key); } } } + + (() => { + const key = "download-a"; + const File = "download_a.js"; + /** + * @type {[{id_ur: number, episodes: number, downloaded: [{episode: number, date:string}], duration: number}] | []} + */ + const data = JSON.parse(localStorage.getItem(key)) || []; + + if (data.length === 0) + return; + + OpenScript(File); + })(); + + function OpenScript(source) { + window.onload = function () { + const script = document.createElement('script'); + script.src = `/javascript/auto/${source}`; + script.type = "module"; + document.body.appendChild(script); + }; + } })(); /** @@ -110,7 +133,7 @@ function LoadEventsHandlerList() { */ function CallEventHandler(params) { const events = { - onStart: function (){ + onStart: function () { window.location.href = "login.html"; } } diff --git a/javascript/pages/settings.js b/javascript/pages/settings.js index 3173c62..bd2592a 100644 --- a/javascript/pages/settings.js +++ b/javascript/pages/settings.js @@ -218,7 +218,13 @@ const Parameters = [ type: 'boolean', param: 'dautosave', name: 'Автосохранение', - description: 'После загрузки автоматически сохраняет файл.' + description: 'После загрузки автоматически сохраняет файл.' + }, + { + type: 'boolean', + param: 'dautoset', + name: 'Автоотметки', + description: 'Отмечать загруженые аниме через 12 часов + продолжительность аниме.' } ] } diff --git a/javascript/pages/watch/mod_download.js b/javascript/pages/watch/mod_download.js index c8f333f..88047b6 100644 --- a/javascript/pages/watch/mod_download.js +++ b/javascript/pages/watch/mod_download.js @@ -1,365 +1,571 @@ +import { ScrollElementWithMouse, Sleep } from "../../modules/funcitons.js"; import { Tunime } from "../../modules/TunimeApi.js"; import { WindowManagement } from "../../modules/Windows.js"; -import { ScrollElementWithMouse } from "../../modules/funcitons.js"; import { Player } from "./mod_player.js"; +import { Anime } from "./mod_resource.js"; +import { UserRate } from "./mod_urate.js"; + +let AutoSave = $PARAMETERS.download.dautosave; + +class Automation { + constructor(downl) { + this.downl = downl; + this.key = "download-a"; + this.Data = JSON.parse(localStorage.getItem(this.key)) || []; + this.Date = new Date().toJSON(); + this.Autoset = $PARAMETERS.download.dautoset; + } -let loaded = false; // Загружены ли эпизоды -let selected = 1; // Выбранный эпизод -let elementSelected = undefined; //Последний выбранный елемент + Show() { + /**@type {[{id:number, episode: [number]}]} */ + let localData = JSON.parse(sessionStorage.getItem(this.key)) || []; + const ur = UserRate().Get(); + const index = localData.findIndex(x => x.id == ur.id); -let downloadLink = undefined; // Локальная ссылка для загрузки файла + if (index === -1) { + return; + } -let startTime, endTime; + for (let i = 0; i < localData[index].episode.length; i++) { + const ep = localData[index].episode[i]; + if ($(`.d-episode[data-e="${ep}"] > .downloaded`).length === 0) { + $(`.d-episode[data-e="${ep}"]`).append(``); + } + } + } -const _data = { - link: undefined, - name: "Anime", - translation: undefined, -} + Set(ep) { + const duration = Anime.duration; + const ur = UserRate().Get(); -let totalFiles = 0; -let downloadedFiles = 0; + if (ur !== null && ur.episodes < ep && this.Autoset) { + let downlData = { id_ur: undefined, episodes: undefined, downloaded: [], duration: duration }; + let index = this.Data.findIndex(x => x.id_ur === ur.id); -/** - * Создает эпизоды в окне загрузке - * @returns ? ничего не возвращяет - */ -function LoadingEpisodes() { - if (loaded) { - return; - } - loaded = true; - const e = Player().episodes.last_episode; - for (let i = 0; i < e; i++) { - const count = i + 1; - $('.window-body-fs > .download-episode > .down-value').append(`${count}EP`); - } + if (index !== -1) { + downlData = this.Data[index]; + this.Data.splice(index, 1); + } - if (e === undefined) { - $('.download-episode').hide(); - } + downlData.id_ur = ur.id; + downlData.episodes = ur.episodes; - $('.window-body-fs > .download-episode > .down-value > .down-episode').on('click', function (e) { - const element = $(e.currentTarget); - const index = element.attr('data-index'); - SelectEpisode(index, true); - }); -} + index = downlData.downloaded.findIndex(x => x.episode === ep); -/** - * Выбирает эпизод - * @param {number} val - выбранный эпизод - * @param {boolean} user - пользователь вызвал - * @returns ? ничего - */ -function SelectEpisode(val, user = false) { - if (!val && selected == val) { - return; - } - selected = val; - const element = $(".download-episode > .down-value > .down-episode")[val - 1]; - if (elementSelected != undefined) { - anime({ - targets: elementSelected, - color: "#555657", - easing: "easeOutElastic(1, 1)", - }) - } - elementSelected = element; - const left = $(element).position().left; - - anime({ - targets: ".sel-down", - left: left, - easing: "easeOutElastic(1, 1)", - complete: function () { - if (!user) { - AutoScrollToEpisode(); + if (index === -1) { + downlData.downloaded.push({ episode: ep, date: this.Date }); } - }, - }); - anime({ - targets: element, - color: "#020202", - easing: "easeOutElastic(1, 1)", - }); -} + this.Data.push(downlData); + localStorage.setItem(this.key, JSON.stringify(this.Data)); + } -/** - * Скроллит к выбранному эпизоду - * @returns ? ничего - */ -function AutoScrollToEpisode() { - let SelPos = $('.down-value > .sel-down').position(); - const WidthEpisodes = $('.download-episode').width(); - const sizeEpisode = (55 + 3); - if ((WidthEpisodes / 2) > SelPos.left) { - return; - } - anime({ - targets: '.download-episode', - scrollLeft: (SelPos.left - (WidthEpisodes / 2) + sizeEpisode), - duration: 500, - easing: 'easeInOutQuad' - }); -} + /**@type {[{id:number, episode: [number]}]} */ + let localData = JSON.parse(sessionStorage.getItem(this.key)) || []; + const index = localData.findIndex(x => x.id == ur.id); + let data = { id: ur.id, episode: [] }; -function SetButtonStatus(status) { - const btn = $(`.window-futter > #btn-download`); - if (status == "loading") { - btn.attr('data-status', status) - btn.text("Загрузка..."); - btn.addClass("disabled"); - } - if (status == "ready") { - btn.attr('data-status', status); - btn.text("Загрузить"); - btn.removeClass("disabled"); - } - if (status == "candown") { - btn.attr('data-status', status); - btn.text("Сохранить"); - btn.removeClass("disabled"); - } -} + if (index !== -1) { + data = localData[index]; + localData.splice(index, 1) + } -function _downloadAnime(e) { - if ($(e).attr('data-status') == "loading") { - return; - } - if ($(e).attr('data-status') == "candown") { - DownloadLocalVideo(); - return; - } - if ($(e).attr('data-status') == "ready") { - SetButtonStatus("loading"); - downloadedFiles = 0; - totalFiles = 0; - $('.progress-download > .value').css({ width: `${0}%` }); // Обновляем индикатор загрузки - GetM3U8Links(); - return; + data.episode.push(ep); + localData.push(data); + + if ($(`.d-episode[data-e="${ep}"] > .downloaded`).length === 0) { + $(`.d-episode[data-e="${ep}"]`).append(``); + } + + sessionStorage.setItem(this.key, JSON.stringify(localData)); } } -async function GetM3U8Links() { - let link = `${_data.link}?episode=${selected}`; - if (!link.includes("http")) { - link = `https:${link}`; +class DownloadAnime { + #abortet = false; + constructor(index, data, downl) { + this.index = index; + this.data = data; + this.downl = downl; + + this.eProgress = $('.progress-download > .value'); + this.eCount = $('.progress-download > .value > .percent'); + + const count = `${0}%` + + this.eProgress.css({ width: count }); + this.eCount.text(count); + + this.Stats = { + total: 0, + downloaded: 0 + }; + + this.startTime = 0; + this.endTime = 0; + this.typeDownload = $PARAMETERS.download.dasync; + this.downloadLink = undefined; } - const data = await Tunime.Source(link); - if (data) { - _localDownload(data); + + Abort() { + this.#abortet = true; } -} -function GetQualityDownload(data) { - let allowQuality = ['720', '480', '360']; - let currentQuality = $PARAMETERS.download.dquality; + async Download() { + this.#OnLoading.forEach(event => event()); + + let link = `${this.data.link}?episode=${this.index}`; - //Записываем только досутпные разрешения - for (let i = 0; i < allowQuality.length; i++) { - const e = allowQuality[i]; - if (data[e].length == 0) { - allowQuality.splice(i, 1); + if (!link.includes("http")) { + link = `https:${link}`; + } + + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; } - } - let idQuality = allowQuality.findIndex(x => x == currentQuality); + const data = await Tunime.Source(link); - if (idQuality == -1) { - if (allowQuality.length != 0) { - currentQuality = allowQuality[0]; + if (data) { + this.#LocalDownload(data); } else { - return -1; + this.#OnError.forEach((event) => { event('critical', 'Ошибка получение данных Tunime.') }); } } - return currentQuality; -} + DownloadBlob() { + const translation = `-${this.data.translation}`; + // Создаем ссылку для скачивания + const dL = document.createElement('a'); + dL.href = this.downloadLink; + dL.download = `${this.data.name}-${this.index}${translation}.ts`; + // Автоматически нажимаем на ссылку для скачивания + dL.click(); + // Очищаем ссылку и удаляем ее из DOM\ + URL.revokeObjectURL(dL.href); + this.#OnCompleted.forEach(event => event(this.index)); + } -function _localDownload(data) { - const quality = GetQualityDownload(data); + #LocalDownload(data) { + const quality = GetQualityDownload(data, this.downl.Quality); - if (quality == -1) { - //Недоступно не одного видео для скачивания - return; - } + if (quality == -1) { + return this.#OnError.forEach((event) => { event('critical', 'Ошибка выбора качества видео.') }); + } - const url = data[quality][0].src.indexOf("http") != -1 ? data[quality][0].src : "https:" + data[quality][0].src; - // Строка, которую нужно удалить - const searchString = `${quality}.mp4:hls:manifest.m3u8`; + const url = data[quality][0].src.indexOf("http") != -1 ? data[quality][0].src : "https:" + data[quality][0].src; + // Строка, которую нужно удалить + const searchString = `${quality}.mp4:hls:manifest.m3u8`; + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } - fetch(url) - .then(response => { - // Удалите подстроку из URL + fetch(url).then(response => { + // Удалить подстроку из URL const urlkodik = response.url.substring(0, response.url.indexOf(searchString)); + response.text().then(async (m3u8Content) => { // data содержит текст манифеста M3U8 const tsUrls = m3u8Content.split('\n').filter(line => line.trim().endsWith('.ts')); + // Сохраняем время начала загрузки - startTime = new Date().getTime(); + this.startTime = new Date().getTime(); - if ($PARAMETERS.download.dasync) { - AsyncDownloadVideo(tsUrls, urlkodik); + if (this.typeDownload) { + this.#AsyncDownload(tsUrls, urlkodik) } else { - DownloadVideo(tsUrls, urlkodik); + this.#SyncDonwload(tsUrls, urlkodik); } + }).catch(error => { - console.error('Ошибка при загрузке M3U8: ', error); + return this.#OnError.forEach((event) => { event('critical', 'Ошибка загрузки m3u8 файла.') }); }); }); -} -async function AsyncDownloadVideo(tsUrls, urlkodik) { - const downloadPromises = []; - totalFiles = tsUrls.length; + function GetQualityDownload(data, currentQuality) { + let allowQuality = ['720', '480', '360']; + + //Записываем только досутпные разрешения + for (let i = 0; i < allowQuality.length; i++) { + const e = allowQuality[i]; + if (data[e].length == 0) { + allowQuality.splice(i, 1); + } + } + + let idQuality = allowQuality.findIndex(x => x == currentQuality); + + if (idQuality == -1) { + if (allowQuality.length != 0) { + currentQuality = allowQuality[0]; + } else { + return -1; + } + } + + return currentQuality; + } + } + + async #AsyncDownload(tsUrls, urlkodik) { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + const downloadPromises = []; + this.Stats.total = tsUrls.length; + + for (let i = 0; i < tsUrls.length; i++) { + try { + const tsUrl = tsUrls[i]; + downloadPromises.push(this.#DownloadTsFile(urlkodik + tsUrl)) + } catch (error) { + this.#OnError.forEach((event) => { event('warning', `Ошибка загрузки фрагмента ${i}.`) }); + console.error(`Failed to fetch ${tsUrls[i]}: ${error.message}`); + } + } - for (let i = 0; i < tsUrls.length; i++) { try { - const tsUrl = tsUrls[i]; - downloadPromises.push(downloadTsFile(urlkodik + tsUrl)); + const tsBlobs = await Promise.all(downloadPromises); + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + // Завершаем загрузку + this.endTime = new Date().getTime(); + // Вычисляем время загрузки + const uploadTime = (this.endTime - this.startTime) / 1000; // в секундах + console.log(uploadTime); + + if (tsBlobs.length === 0) { + return this.#OnError.forEach((event) => { event('critical', `Не удалось загрузить ни один фрагмент.`) }); + } + + const mergedBlob = new Blob(tsBlobs, { type: 'video/mp2t' }); + this.downloadLink = URL.createObjectURL(mergedBlob); + + this.#OnCanDownload.forEach((event) => event()); + } catch (error) { - console.error(`Failed to fetch ${tsUrl[i]}: ${error.message}`); + console.error('Ошибка при загрузке M3U8: ', error); + return this.#OnError.forEach((event) => { event('critical', `Ошибка при загрузке M3U8.`) }); } } - try { - const tsBlobs = await Promise.all(downloadPromises); + async UpdateProgress() { + const progress = (this.Stats.downloaded / this.Stats.total) * 100; + this.eProgress.css({ width: `${progress}%` }); // Обновляем индикатор загрузки + this.eCount.text(`${progress.toFixed(0)}%`); + } - // Завершаем загрузку (это симуляция, на самом деле должно быть событие окончания загрузки файла) - endTime = new Date().getTime(); + async #DownloadTsFile(tsUrl) { + const tsResponse = await this.#fetchWithRetry(tsUrl); + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + const tsBlob = await tsResponse.blob(); + this.Stats.downloaded++; + this.UpdateProgress(); + return tsBlob; + } + + async #fetchWithRetry(url, retryCount = 25) { + try { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + return response; + } catch (error) { + console.error(`Error fetching ${url}: ${error.message}`); + if (retryCount > 0) { + console.log(`Retrying in 1 second... (${retryCount} attempts left)`); + await Sleep(1000); + return this.#fetchWithRetry(url, retryCount - 1); + } else { + throw new Error(`Failed to fetch ${url} after multiple attempts`); + } + } + } + async #SyncDonwload() { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + let tsBlobs = []; + this.Stats.total = tsUrls.length; + for (let i = 0; i < tsUrls.length; i++) { + if (this.#abortet) { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + try { + const tsUrl = tsUrls[i]; + const tsResponse = await this.#fetchWithRetry(urlkodik + tsUrl); + const tsBlob = await tsResponse.blob(); + tsBlobs.push(tsBlob); + this.Stats.downloaded++; + this.UpdateProgress(); + } catch (error) { + this.#OnError.forEach((event) => { event('warning', `Ошибка загрузки фрагмента ${i}.`) }); + } + } + + // Завершаем загрузку + this.endTime = new Date().getTime(); // Вычисляем время загрузки - const uploadTime = (endTime - startTime) / 1000; // в секундах + const uploadTime = (this.endTime - this.startTime) / 1000; // в секундах console.log(uploadTime); if (tsBlobs.length === 0) { - console.log('Не удалось загрузить ни один файл .ts!'); - return; + return this.#OnError.forEach((event) => { event('critical', `Не удалось загрузить ни один фрагмент.`) }); } const mergedBlob = new Blob(tsBlobs, { type: 'video/mp2t' }); - downloadLink = URL.createObjectURL(mergedBlob); + this.downloadLink = URL.createObjectURL(mergedBlob); - DownloadCompleated(); - } catch (error) { - console.error('Ошибка при загрузке M3U8: ', error); + this.#OnCanDownload.forEach((event) => event()); } -} -async function DownloadVideo(tsUrls, urlkodik) { - let tsBlobs = []; - for (let i = 0; i < tsUrls.length; i++) { - try { - const tsUrl = tsUrls[i]; - const tsResponse = await fetchWithRetry(urlkodik + tsUrl); - const tsBlob = await tsResponse.blob(); - tsBlobs.push(tsBlob); - const progress = ((i + 1) / tsUrls.length) * 100; - $('.progress-download > .value').css({ width: `${progress}%` }); - } catch (error) { - console.error(`Failed to fetch ${tsUrls[i]}: ${error.message}`); + Deprecate() { + this.#OnAbbortet.forEach(event => event()); + this.#OnAbbortet = []; + return; + } + + #OnLoading = []; + #OnCanDownload = []; + #OnCompleted = []; + #OnError = []; + #OnAbbortet = []; + + /** + * + * @param {'loading' | 'candownload' | 'completed' | 'error' | 'abbortet'} name + * @param {function} event + */ + On(name, event = () => { }) { + if (name === 'loading') { + this.#OnLoading.push(event); + } else if (name === 'candownload') { + this.#OnCanDownload.push(event); + } else if (name === 'completed') { + this.#OnCompleted.push(event); + } else if (name === 'error') { + this.#OnError.push(event); + } else if (name === 'abbortet') { + this.#OnAbbortet.push(event); } } +} - // Завершаем загрузку (это симуляция, на самом деле должно быть событие окончания загрузки файла) - endTime = new Date().getTime(); +class Loading { + #loaded = false; - // Вычисляем время загрузки - const uploadTime = (endTime - startTime) / 1000; // в секундах - console.log(uploadTime); + constructor(downl) { + /** + * @type {Download} + */ + this.Download = downl; + } - if (tsBlobs.length === 0) { - console.log('Не удалось загрузить ни один файл .ts!'); - return; + get IsLoaded() { + return this.#loaded; } - const mergedBlob = new Blob(tsBlobs, { type: 'video/mp2t' }); - downloadLink = URL.createObjectURL(mergedBlob); + Load() { + if (this.#loaded) + return; + this.#loaded = true; - DownloadCompleated(); -} + const e = Player().episodes.last_episode; + if (e !== undefined && e > 1) + $('.wrapper-episodes-d').removeClass('hide'); -function DownloadCompleated() { - SetButtonStatus("candown"); - if ($PARAMETERS.download.dautosave) { - DownloadLocalVideo(); - } -} + $('.wrapper-episodes-d > .episodes-download').empty(); -function DownloadLocalVideo() { - const translation = `-${_data.translation}`; - // Создаем ссылку для скачивания - const dL = document.createElement('a'); - dL.href = downloadLink; - dL.download = `${_data.name}-${selected}${translation}.ts`; - // Автоматически нажимаем на ссылку для скачивания - dL.click(); - // Очищаем ссылку и удаляем ее из DOM - URL.revokeObjectURL(dL.href); - SetButtonStatus("ready"); -} + for (let i = 0; i < e; i++) { + const c = i + 1; + $('.wrapper-episodes-d > .episodes-download').append(`
${c}ep
`); + } -async function updateProgress() { - const progress = (downloadedFiles / totalFiles) * 100; - $('.progress-download > .value').css({ width: `${progress}%` }); // Обновляем индикатор загрузки + this.Download.events.OnSelect.bind(this.Download)(); + this.Download.functions.Select(Player().episodes.selected_episode); + } } -async function downloadTsFile(tsUrl) { - const tsResponse = await fetchWithRetry(tsUrl); - const tsBlob = await tsResponse.blob(); - downloadedFiles++; - updateProgress(); - return tsBlob; -} +class Download { + #Data = { + link: undefined, + name: "Anime", + translation: undefined, + } + /**@type {DownloadAnime} */ + #Download = undefined; + + constructor() { + this.Selected = 0; + this.Quality = $PARAMETERS.download.dquality; + this.Loaded = new Loading(this); + this.Automation = new Automation(this); + } + + Download(index = this.Selected) { -async function fetchWithRetry(url, retryCount = 25) { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Request failed with status ${response.status}`); + if (this.#Download !== undefined && this.#Download.downloadLink !== undefined) { + this.#Download.DownloadBlob(); + return; } - return response; - } catch (error) { - console.error(`Error fetching ${url}: ${error.message}`); - if (retryCount > 0) { - console.log(`Retrying in 1 second... (${retryCount} attempts left)`); - await new Promise(resolve => setTimeout(resolve, 1000)); - return fetchWithRetry(url, retryCount - 1); + + if (index <= 0 || this.#Download !== undefined || this.#Data.link === undefined) + return; + + this.#Download = new DownloadAnime(index, this.#Data, this); + + this.#Download.On('loading', () => { + $(`#btn-download`).addClass('disable'); + $(`#btn-stop`).removeClass('disable'); + }); + + this.#Download.On('candownload', () => { + $(`#btn-download`).removeClass('disable'); + $(`#btn-download`).text('Сохранить файл'); + $(`#btn-stop`).addClass('disable'); + if (AutoSave) { + this.#Download.DownloadBlob(); + } + }); + + this.#Download.On('completed', (episode) => { + $(`#btn-download`).removeClass('disable'); + $(`#btn-download`).text('Загрузить'); + $(`#btn-stop`).addClass('disable'); + + this.#Download = undefined; + this.Automation.Set(episode); + }); + + this.#Download.On('abbortet', () => { + $(`#btn-download`).removeClass('disable'); + $(`#btn-stop`).addClass('disable'); + + this.#Download = undefined; + }); + + this.#Download.On('error', (type, msg) => { + console.log(msg, type); + if (type == "critical") { + $(`.error-message`).text(msg); + $(`.error-message`).removeClass('hide'); + this.#Download.Deprecate(); + } + }); + + this.#Download.Download(); + } + + Stop() { + if (this.#Download !== undefined) + this.#Download.Abort(); + } + + SetData(data) { + if (data) { + this.#Data.name = data.title_orig; + this.#Data.link = data.link; + this.#Data.translation = data.translation.title; + + $('.download-info > .voice').text(this.#Data.translation); + $('.download-info > .quality').text(`${this.Quality}p`); } else { - throw new Error(`Failed to fetch ${url} after multiple attempts`); + console.log('Ошибка в данных', data); + Structure.hide(); + } + } + + events = { + OnSelect: function () { + $('.episodes-download > .d-episode').on('click', (e) => { + const element = $(e.currentTarget); + const index = element.attr('data-e'); + this.functions.Select(index); + }); + } + } + + functions = { + Select: (index) => { + const element = $(`.d-episode[data-e="${index}"]`); + if (element.hasClass('selected')) + return; + $('.d-episode.selected').removeClass('selected'); + element.addClass('selected'); + this.Selected = index; } } } -const WindowDownload = { +const Structure = { + download: new Download(), init: function () { - ScrollElementWithMouse('.download-episode'); - $('.window-futter > #btn-download').on('click', function (e) { - _downloadAnime(e.currentTarget); + $('.bar-download > .window-close').on('click', function () { + Structure.hide(); }); - $('.bar-download > .window-close').on('click', function () { - WindowDownload.hide(); + //Переключение парамерта дубляжи избранное по франшизе + $('.autosave-param').on('click', (e) => { + if (e.target.checked != undefined) { + //Переключаем парамерт + setParameter('dautosave', e.target.checked); + AutoSave = $PARAMETERS.download.dautosave; + } + }); + + $('#btn-download').on('click', (e) => { + $(`.error-message`).removeClass('hide'); + this.download.Download(); + }); + + $(`#btn-stop`).on('click', (e) => { + this.download.Stop(); }); + + $(`#help-download`).on('click', (e) => { + window.open("https://github.com/AN0NCER/an0ncer.github.io/wiki/%D0%9A%D0%B0%D0%BA-%D1%81%D0%BA%D0%B0%D1%87%D0%B0%D1%82%D1%8C-%D0%B0%D0%BD%D0%B8%D0%BC%D0%B5%3F", "_blank") + }) + + ScrollElementWithMouse('.wrapper-episodes-d'); + $('.autosave-param > .checkbox > input').prop('checked', AutoSave); }, show: function () { - LoadingEpisodes(); - const id = Player().data.findIndex(x => x.id == Player().data_id); - const data = Player().data[id]; - $('.download-info > .voice').text(data.translation.title); - $('.download-info > .quality').text($PARAMETERS.download.dquality); - _data.name = data.title_orig; - _data.link = data.link; - _data.translation = data.translation.title; $("body").addClass("loading"); + this.download.Loaded.Load(); + const index = Player().data.findIndex(x => x.id == Player().data_id); + const data = Player().data[index]; + this.download.SetData(data); + this.download.Automation.Show(); }, hide: function () { - _windowDownload.hide(); + Window.hide(); $("body").removeClass("loading"); }, @@ -369,11 +575,22 @@ const WindowDownload = { anim: { showed: function () { - SelectEpisode(Player().episodes.selected_episode); + let SelPos = $('.d-episode.selected').position(); + const WidthEpisodes = $('.wrapper-episodes-d').width(); + const sizeEpisode = (55 + 3); + if ((WidthEpisodes / 2) > SelPos.left) { + return; + } + anime({ + targets: '.wrapper-episodes-d', + scrollLeft: (SelPos.left - (WidthEpisodes / 2) + sizeEpisode), + duration: 500, + easing: 'easeInOutQuad' + }); } } } -const _windowDownload = new WindowManagement(WindowDownload, '.window-download'); +const Window = new WindowManagement(Structure, '.window-download'); -export const ShowDwonloadWindow = () => { _windowDownload.click("Не доступно!"); } \ No newline at end of file +export const ShowDwonloadWindow = () => { Window.click("Не доступно!"); }; \ No newline at end of file diff --git a/javascript/pages/watch/mod_resource.js b/javascript/pages/watch/mod_resource.js index 8a10f41..d813750 100644 --- a/javascript/pages/watch/mod_resource.js +++ b/javascript/pages/watch/mod_resource.js @@ -8,6 +8,8 @@ import { LoadScreen } from "./mod_load.js"; import { History } from "./mod_history.js"; export let Screenshots = undefined; +export let Franchises = []; +export let Anime = undefined; /** * Загружает аниме на сайт @@ -19,31 +21,30 @@ export async function LoadAnime(e = () => { }, isLogged = false) { progress = new LoadScreen(9), process = []; let posterLink = undefined, - jikanLoaded = false, - animeData = undefined; + jikanLoaded = false; try { progress.Step(); process.push(new Promise(async (resolve) => { - animeData = await FetchAnime($ID); + Anime = await FetchAnime($ID); progress.Step(); - UserRate().init(animeData.user_rate, isLogged); + UserRate().init(Anime.user_rate, isLogged); if (posterLink === undefined) { - posterLink = `${SHIKIURL.url}/${animeData.image.original}`; + posterLink = `${SHIKIURL.url}/${Anime.image.original}`; if (jikanLoaded) { process.push(LoadPoster(posterLink)); } } - SetTitle(animeData); - Genres(animeData); - Duration(animeData); - Status(animeData); - NextEpisode(animeData); - Description(animeData); - Studio(animeData); - PageTitle(animeData); - PageMetaTags(animeData); + SetTitle(Anime); + Genres(Anime); + Duration(Anime); + Status(Anime); + NextEpisode(Anime); + Description(Anime); + Studio(Anime); + PageTitle(Anime); + PageMetaTags(Anime); resolve(true); })); @@ -72,7 +73,7 @@ export async function LoadAnime(e = () => { }, isLogged = false) { progress.Step(); } - e(animeData); + e(Anime); } catch (error) { console.log(error); } @@ -174,6 +175,8 @@ export async function LoadAnime(e = () => { }, isLogged = false) { continue; } + Franchises.push(element); + //Создаем елемент const html = `
${element.name diff --git a/javascript/parametrs.js b/javascript/parametrs.js index 8df805d..b3c7368 100644 --- a/javascript/parametrs.js +++ b/javascript/parametrs.js @@ -20,7 +20,8 @@ const $PARAMETERS = { download: { dquality: '720', dasync: true, - dautosave: true + dautosave: true, + dautoset: true, }, player: { quality: '720', diff --git a/style/css/watch.css b/style/css/watch.css index 4314221..a93876a 100644 --- a/style/css/watch.css +++ b/style/css/watch.css @@ -1401,7 +1401,7 @@ footer .studio span { .windowed .content-download .content-wraper { display: grid; - grid-template-rows: auto auto auto; + grid-template-rows: auto auto auto auto; overflow-y: hidden; } .windowed .content-download .content-wraper .window-bar { @@ -1409,6 +1409,25 @@ footer .studio span { justify-content: space-between; align-items: center; } +.windowed .content-download .content-wraper .window-bar .window-title { + display: flex; + align-items: center; + gap: 10px; +} +.windowed .content-download .content-wraper .window-bar .window-title .help-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: #191C21; + border-radius: 3px; + cursor: pointer; +} +.windowed .content-download .content-wraper .window-bar .window-title .help-icon svg { + height: 12px; + fill: #fff; +} .windowed .content-download .content-wraper .window-bar .window-close { width: 40px; height: 40px; @@ -1419,117 +1438,183 @@ footer .studio span { .windowed .content-download .content-wraper .window-bar .window-close svg { fill: #fff; } -.windowed .content-download .content-wraper .window-body-fs { - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 10px; -} -.windowed .content-download .content-wraper .window-body-fs .download-episode { - overflow: hidden; - overflow-x: hidden; +.windowed .content-download .content-wraper .wrapper-episodes-d { + overflow-y: hidden; overflow-x: auto; - border-radius: 3px; } -.windowed .content-download .content-wraper .window-body-fs .download-episode .down-value { - display: inline-flex; - background: #15171e; +.windowed .content-download .content-wraper .wrapper-episodes-d .episodes-download { + background: #191C21; border-radius: 3px; - width: auto; - position: relative; + display: inline-flex; + gap: 5px; } -.windowed .content-download .content-wraper .window-body-fs .download-episode .down-value .down-episode { - margin: 3px; - width: 55px; - height: 70px; +.windowed .content-download .content-wraper .wrapper-episodes-d .episodes-download .d-episode { display: flex; + flex-direction: column; justify-content: center; align-items: center; - font-family: Manrope, serif; + width: 55px; + height: 70px; + font-size: 16px; font-weight: bold; - color: #555657; - z-index: 2; + margin: 3px; + gap: 5px; + color: #fff; position: relative; - flex-direction: column; + border-radius: 3px; + opacity: 0.4; + transition: 0.2s ease-in-out; cursor: pointer; } -.windowed .content-download .content-wraper .window-body-fs .download-episode .down-value .down-episode .ep-name { +.windowed .content-download .content-wraper .wrapper-episodes-d .episodes-download .d-episode.selected { + background: #3C92ED; + opacity: 1; + cursor: auto; +} +.windowed .content-download .content-wraper .wrapper-episodes-d .episodes-download .d-episode span { + text-transform: uppercase; font-size: 11px; - margin-top: 5px; } -.windowed .content-download .content-wraper .window-body-fs .download-episode .down-value .down-episode svg { - display: none; +.windowed .content-download .content-wraper .wrapper-episodes-d .episodes-download .d-episode .downloaded { + display: flex; + width: 5px; + height: 5px; position: absolute; - top: 2px; - right: 5px; - width: 12px; - transform: rotate(20deg); - fill: #5fbef3; + right: 4px; + top: 4px; + background: #3C92ED; + border-radius: 50%; } -.windowed .content-download .content-wraper .window-body-fs .download-episode .down-value .down-episode.downloaded svg { - display: block; +.windowed .content-download .content-wraper .progress-wrapper { + position: relative; + height: 25px; + padding-top: 20px; } -.windowed .content-download .content-wraper .window-body-fs .download-episode .sel-down { +.windowed .content-download .content-wraper .progress-wrapper .download-info { position: absolute; - width: 55px; - height: 70px; - background: #5fbef3; - margin: 3px; + top: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + font-size: 14px; +} +.windowed .content-download .content-wraper .progress-wrapper .progress-download { + display: flex; + height: 25px; border-radius: 3px; - z-index: 1; + background: #191C21; + overflow: hidden; } -.windowed .content-download .content-wraper .window-body-fs .user-help { +.windowed .content-download .content-wraper .progress-wrapper .progress-download .value { display: flex; - gap: 20px; - font-family: "Manrope"; - align-items: center; - opacity: 0.6; + width: 0%; + height: 100%; + background: #2393F1; + position: relative; + container-type: inline-size; + transition: 0.3s ease-in-out; } -.windowed .content-download .content-wraper .window-body-fs .user-help .user-help-link { - font-weight: bolder; - font-size: 24px; - color: #51B3FF; +.windowed .content-download .content-wraper .progress-wrapper .progress-download .value .percent { + position: absolute; + color: rgba(255, 255, 255, 0.3); + font-weight: 200; + top: 0; + bottom: 0; + right: -35px; + display: flex; + align-items: center; + transition: 0.3s ease-in-out; } -.windowed .content-download .content-wraper .window-body-fs .user-help .user-help-info { - font-size: 13px; +@container (min-width: 80cqw) { + .windowed .content-download .content-wraper .progress-wrapper .progress-download .value .percent { + right: 10px; + color: rgba(255, 255, 255, 0.8); + font-weight: 300; + } } -.windowed .content-download .content-wraper .window-body-fs .download-info { - display: flex; - justify-content: space-between; +.windowed .content-download .content-wraper .error-message { + text-align: center; + color: #A92527; font-size: 14px; + font-family: Inter; } -.windowed .content-download .content-wraper .window-body-fs .progress-download { - width: 100%; - height: 25px; +.windowed .content-download .content-wraper .download-buttons { + position: relative; +} +.windowed .content-download .content-wraper .download-buttons .btn { + height: 40px; + display: grid; + place-items: center; border-radius: 3px; - background: #272b38; - display: flex; - overflow: hidden; + font-size: 16px; } -.windowed .content-download .content-wraper .window-body-fs .progress-download .value { - background: #3283E4; - height: 25px; - width: 0%; +.windowed .content-download .content-wraper .download-buttons .btn.btn-accept { + position: relative; + z-index: 9; + background: #3C92ED; +} +.windowed .content-download .content-wraper .download-buttons .btn.btn-accept.disable { + transform: translateY(-5px); + scale: 0.9; + opacity: 0.5; + z-index: 8; + pointer-events: none; +} +.windowed .content-download .content-wraper .download-buttons .btn.btn-stop { + position: absolute; + background: #F12323; + bottom: 0; + left: 0; + right: 0; + z-index: 10; transition: 0.3s ease-in-out; } -.windowed .content-download .content-wraper .window-futter { +.windowed .content-download .content-wraper .download-buttons .btn.btn-stop.disable { + bottom: 5px; + scale: 0.9; + opacity: 0.5; + z-index: 8; + pointer-events: none; +} +.windowed .content-download .content-wraper .window-footer { display: flex; - gap: 10px; + align-items: center; + justify-content: space-between; + padding: 8px 10px; } -.windowed .content-download .content-wraper .window-futter .btn-accept { - cursor: pointer; +.windowed .content-download .content-wraper .window-footer .title { display: flex; - height: 40px; - flex-direction: column; - justify-content: center; + gap: 20px; align-items: center; - flex: 1 0 0; - border-radius: 4px; - background: linear-gradient(135deg, #51B3FF 0%, #3283E4 100%); - transition: 0.3s ease-in-out; + color: #FFF; + font-family: Manrope; + font-size: 14px; } -.windowed .content-download .content-wraper .window-futter .btn-accept.disabled { - opacity: 0.3; +.windowed .content-download .content-wraper .window-footer .title svg { + width: 16px; + fill: #fff; +} +.windowed .content-download .content-wraper .window-footer .checkbox { + overflow: hidden; + display: grid; + place-items: center; + width: 16px; + height: 16px; + background: #C0C0C0; + border-radius: 3px; +} +.windowed .content-download .content-wraper .window-footer .checkbox input { + display: none; +} +.windowed .content-download .content-wraper .window-footer .checkbox .switch-check { + display: none; + width: 100%; + height: 100%; + background: #3C92ED; +} +.windowed .content-download .content-wraper .window-footer .checkbox input:checked + .switch-check { + display: grid; } :root { diff --git a/style/watch/_download.scss b/style/watch/_download.scss index 7f8d267..21f7a2f 100644 --- a/style/watch/_download.scss +++ b/style/watch/_download.scss @@ -2,7 +2,7 @@ .content-download { .content-wraper { display: grid; - grid-template-rows: auto auto auto; + grid-template-rows: auto auto auto auto; overflow-y: hidden; .window-bar { @@ -10,9 +10,33 @@ justify-content: space-between; align-items: center; + $btn_size: 40px; + + .window-title { + display: flex; + align-items: center; + gap: 10px; + + .help-icon { + display: flex; + align-items: center; + justify-content: center; + width: $btn_size; + height: $btn_size; + background: #191C21; + border-radius: 3px; + cursor: pointer; + + svg { + height: 12px; + fill: #fff; + } + } + } + .window-close { - width: 40px; - height: 40px; + width: $btn_size; + height: $btn_size; display: grid; place-items: center; cursor: pointer; @@ -23,132 +47,209 @@ } } - .window-body-fs { - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 10px; + .wrapper-episodes-d { + overflow-y: hidden; + overflow-x: auto; - .download-episode { - overflow: hidden; - overflow-x: hidden; - overflow-x: auto; + .episodes-download { + background: #191C21; border-radius: 3px; + display: inline-flex; + gap: 5px; - .down-value { - display: inline-flex; - background: #15171e; - border-radius: 3px; - width: auto; - position: relative; - - .down-episode { - margin: 3px; - width: 55px; - height: 70px; - display: flex; - justify-content: center; - align-items: center; - font-family: Manrope, serif; - font-weight: bold; - color: #555657; - z-index: 2; - position: relative; - flex-direction: column; - cursor: pointer; - - .ep-name { - font-size: 11px; - margin-top: 5px; - } - - svg{ - display: none; - position: absolute; - top: 2px; - right: 5px; - width: 12px; - transform: rotate(20deg); - fill: #5fbef3; - // opacity: 0.6; - } - - &.downloaded{ - svg{ - display: block; - } - } - } - } - .sel-down { - position: absolute; + .d-episode { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; width: 55px; height: 70px; - background: #5fbef3; + font-size: 16px; + font-weight: bold; margin: 3px; + gap: 5px; + color: #fff; + position: relative; border-radius: 3px; - z-index: 1; - } - } + opacity: .4; + transition: .2s ease-in-out; + cursor: pointer; - .user-help{ - display: flex; - gap: 20px; - font-family: 'Manrope'; - align-items: center; - opacity: .6; - - .user-help-link{ - font-weight: bolder; - font-size: 24px; - color: #51B3FF; - } + &.selected { + background: #3C92ED; + opacity: 1; + cursor: auto; + } + + span { + text-transform: uppercase; + font-size: 11px; + } - .user-help-info{ - font-size: 13px; + .downloaded { + display: flex; + width: 5px; + height: 5px; + position: absolute; + right: 4px; + top: 4px; + background: #3C92ED; + border-radius: 50%; + } } } + } + + .progress-wrapper { + position: relative; + height: 25px; + padding-top: 20px; .download-info { + position: absolute; + top: 0; + left: 0; + right: 0; display: flex; justify-content: space-between; font-size: 14px; } - .progress-download{ - width: 100%; + .progress-download { + display: flex; height: 25px; border-radius: 3px; - background: #272b38; - display: flex; + background: #191C21; overflow: hidden; - .value{ - background: #3283E4; - height: 25px; + + .value { + display: flex; width: 0%; + height: 100%; + background: #2393F1; + position: relative; + container-type: inline-size; transition: .3s ease-in-out; + + .percent { + position: absolute; + color: rgba($color: #fff, $alpha: .3); + font-weight: 200; + top: 0; + bottom: 0; + right: -35px; + display: flex; + align-items: center; + transition: .3s ease-in-out; + } + + @container (min-width: 80cqw) { + .percent { + right: 10px; + color: rgba($color: #fff, $alpha: .8); + font-weight: 300; + } + } + } + } + } + + .error-message { + text-align: center; + color: #A92527; + font-size: 14px; + font-family: Inter; + } + + .download-buttons { + position: relative; + + .btn { + height: 40px; + display: grid; + place-items: center; + border-radius: 3px; + font-size: 16px; + + &.btn-accept { + position: relative; + z-index: 9; + background: #3C92ED; + + &.disable { + transform: translateY(-5px); + scale: .9; + opacity: .5; + z-index: 8; + pointer-events: none; + } + } + + &.btn-stop { + position: absolute; + background: #F12323; + bottom: 0; + left: 0; + right: 0; + z-index: 10; + transition: .3s ease-in-out; + + &.disable { + bottom: 5px; + scale: .9; + opacity: .5; + z-index: 8; + pointer-events: none; + } } } } - .window-futter { + .window-footer { display: flex; - gap: 10px; - .btn-accept { - cursor: pointer; + align-items: center; + justify-content: space-between; + padding: 8px 10px; + + + + .title { display: flex; - height: 40px; - flex-direction: column; - justify-content: center; + gap: 20px; align-items: center; - flex: 1 0 0; - border-radius: 4px; - background: linear-gradient(135deg, #51B3FF 0%, #3283E4 100%); - transition: .3s ease-in-out; + color: #FFF; + font-family: Manrope; + font-size: 14px; + + svg { + width: 16px; + fill: #fff; + } + } + + .checkbox { + overflow: hidden; + display: grid; + place-items: center; + width: 16px; + height: 16px; + background: #C0C0C0; + border-radius: 3px; + + input { + display: none; + } + + .switch-check { + display: none; + width: 100%; + height: 100%; + background: #3C92ED; + } - &.disabled{ - opacity: .3; + input:checked+.switch-check { + display: grid; } } } diff --git a/watch.html b/watch.html index e645053..0b3083a 100644 --- a/watch.html +++ b/watch.html @@ -389,7 +389,7 @@ -