├── .gitignore ├── .github └── img │ ├── preview1.png │ └── preview2.png ├── static ├── logo │ ├── 512x512.icns │ ├── 512x512.ico │ └── 512x512.png ├── img │ ├── frame │ │ ├── min-w-30.png │ │ └── close-w-30.png │ └── layered-waves-haikei.svg ├── bootstrap │ ├── fonts │ │ └── lato │ │ │ ├── S6uyw4BMUTPHjx4wXg.woff2 │ │ │ ├── S6u8w4BMUTPHjxsAXC-q.woff2 │ │ │ ├── S6uyw4BMUTPHjxAwXjeu.woff2 │ │ │ ├── S6u8w4BMUTPHjxsAUi-qJCY.woff2 │ │ │ ├── S6u9w4BMUTPHh6UVSwiPGQ.woff2 │ │ │ ├── S6u9w4BMUTPHh6UVSwaPGR_p.woff2 │ │ │ └── lato.css │ └── bootstrap.bundle.min.js ├── css │ ├── custom-styles.css │ ├── effects.css │ ├── index.css │ └── views │ │ └── downloader.css └── js │ ├── effects.js │ ├── index.js │ ├── modules │ └── modal-generator.js │ └── views │ └── downloader.js ├── dev-app-update.yml ├── back ├── config.js ├── ipc-handler.js ├── electron-tools.js ├── update-service.js ├── requests.js ├── utility-windows.js ├── dependency-installer.js └── downloader.js ├── afterPackHook.js ├── LICENSE ├── templates ├── index.html ├── utils │ └── progress-bar.html └── views │ └── downloader.html ├── README.md ├── preload.js ├── package.json └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | dist/ 4 | .idea/ 5 | electron-builder.env -------------------------------------------------------------------------------- /.github/img/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/.github/img/preview1.png -------------------------------------------------------------------------------- /.github/img/preview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/.github/img/preview2.png -------------------------------------------------------------------------------- /static/logo/512x512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/logo/512x512.icns -------------------------------------------------------------------------------- /static/logo/512x512.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/logo/512x512.ico -------------------------------------------------------------------------------- /static/logo/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/logo/512x512.png -------------------------------------------------------------------------------- /static/img/frame/min-w-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/img/frame/min-w-30.png -------------------------------------------------------------------------------- /static/img/frame/close-w-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/img/frame/close-w-30.png -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | owner: Davis-Software 2 | repo: YTDownloader 3 | provider: github 4 | publishAutoUpdate: true 5 | updaterCacheDirName: swc_ytdownloader-updater 6 | -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/S6uyw4BMUTPHjx4wXg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/bootstrap/fonts/lato/S6uyw4BMUTPHjx4wXg.woff2 -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/S6u8w4BMUTPHjxsAXC-q.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/bootstrap/fonts/lato/S6u8w4BMUTPHjxsAXC-q.woff2 -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/S6uyw4BMUTPHjxAwXjeu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/bootstrap/fonts/lato/S6uyw4BMUTPHjxAwXjeu.woff2 -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/S6u8w4BMUTPHjxsAUi-qJCY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/bootstrap/fonts/lato/S6u8w4BMUTPHjxsAUi-qJCY.woff2 -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/S6u9w4BMUTPHh6UVSwiPGQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/bootstrap/fonts/lato/S6u9w4BMUTPHh6UVSwiPGQ.woff2 -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/S6u9w4BMUTPHh6UVSwaPGR_p.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Davis-Software/YTDownloader/HEAD/static/bootstrap/fonts/lato/S6u9w4BMUTPHh6UVSwaPGR_p.woff2 -------------------------------------------------------------------------------- /back/config.js: -------------------------------------------------------------------------------- 1 | const { app } = require("electron") 2 | const path = require("path") 3 | 4 | 5 | exports.platform = process.platform === "win32" ? "win32" : "unix" 6 | exports.iconPath = path.join(__dirname, "..", "static", "logo", "512x512" + (exports.platform === "win32" ? ".ico" : ".png")) 7 | 8 | exports.appDataDir = app.getPath("userData") 9 | exports.tempDir = app.getPath("temp") 10 | 11 | exports.devMode = false 12 | exports.autoUpdate = true 13 | -------------------------------------------------------------------------------- /back/ipc-handler.js: -------------------------------------------------------------------------------- 1 | const { ipcMain, webContents } = require("electron") 2 | 3 | function registerIpcListener(channel, callback, once=false){ 4 | ipcMain.handle(channel, (...resp) => { 5 | if(once){ 6 | ipcMain.removeHandler(channel) 7 | } 8 | callback(...resp) 9 | }) 10 | } 11 | 12 | function invoke(channel, ...args){ 13 | webContents.getAllWebContents().forEach(webContent => { 14 | webContent.send(channel, ...args) 15 | }) 16 | } 17 | 18 | 19 | module.exports = { 20 | registerIpcListener, 21 | invoke 22 | } 23 | -------------------------------------------------------------------------------- /static/css/custom-styles.css: -------------------------------------------------------------------------------- 1 | .form-control, .form-select{ 2 | background-color: #272b30; 3 | transition: all .2s; 4 | color: white; 5 | } 6 | .form-control:active, .form-control:focus, .form-select:active, .form-select:focus{ 7 | background-color: #31353b; 8 | color: white; 9 | } 10 | .form-control:disabled, .form-select:disabled{ 11 | background-color: #272b30 !important; 12 | color: #9a9a9a; 13 | } 14 | input[type=checkbox]{ 15 | transition: all .2s; 16 | } 17 | 18 | .input-group{ 19 | border: solid 2px #111; 20 | border-radius: .25rem; 21 | } 22 | 23 | .btn{ 24 | transition: all .2s; 25 | } -------------------------------------------------------------------------------- /afterPackHook.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const yaml = require('js-yaml') 5 | exports.default = async _ => { 6 | let data = { 7 | owner: "Davis-Software", 8 | repo: "YTDownloader", 9 | provider: "github", 10 | publishAutoUpdate: true, 11 | releaseType: "release", 12 | } 13 | if(process.platform === "linux") { 14 | data.updaterCacheDirName = 'swc_ytdownloader-updater' 15 | fs.writeFileSync( 16 | path.join(__dirname, 'dist','linux-unpacked','resources','app-update.yml'), 17 | yaml.dump(data), 18 | 'utf8' 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /back/electron-tools.js: -------------------------------------------------------------------------------- 1 | const { BrowserWindow } = require("electron") 2 | 3 | 4 | function filterWindowsByIdentifier(identifier){ 5 | let windows = BrowserWindow.getAllWindows() 6 | let filtered = [] 7 | for(let win of windows){ 8 | if(win.hasOwnProperty("identifier") && win.identifier === identifier){ 9 | filtered.push(win) 10 | } 11 | } 12 | return filtered 13 | } 14 | 15 | function getMainWindow(){ 16 | let mainWindow = filterWindowsByIdentifier("main-window") 17 | if(mainWindow.length > 0) return mainWindow[0] 18 | return null 19 | } 20 | function getAllProgressBars(){ 21 | return filterWindowsByIdentifier("progress-bar") 22 | } 23 | 24 | 25 | module.exports = { 26 | filterWindowsByIdentifier, 27 | getMainWindow, 28 | getAllProgressBars 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2022 Davis_Software 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /back/update-service.js: -------------------------------------------------------------------------------- 1 | const { dialog } = require("electron") 2 | const { getMainWindow, getAllProgressBars} = require("./electron-tools") 3 | const { invoke } = require("./ipc-handler") 4 | const { autoUpdater } = require('electron-updater') 5 | 6 | 7 | let win = getMainWindow() 8 | 9 | autoUpdater.autoDownload = true 10 | autoUpdater.autoInstallOnAppQuit = true 11 | 12 | function update_available(info){ 13 | invoke("update:info", info) 14 | } 15 | function update_not_available(info){ 16 | console.info(`No update available. - Currently running latest on ${info.version}`) 17 | } 18 | function update_downloaded(){ 19 | getAllProgressBars().forEach(bar => bar.minimize()) 20 | let resp = dialog.showMessageBoxSync(win, { 21 | buttons: ["Yes", "No"], 22 | message: "Update ready!\nDo you want to restart and update?" 23 | }) 24 | autoUpdater.autoInstallOnAppQuit = true 25 | if(resp === 0){ 26 | autoUpdater.quitAndInstall() 27 | }else{ 28 | getAllProgressBars().forEach(bar => bar.restore()) 29 | } 30 | } 31 | function update_error(error){ 32 | dialog.showErrorBox("Update error - " + error.name, error.message + "\n\n" + error.cause + "\n\n" + error.stack) 33 | } 34 | 35 | autoUpdater.on("update-available", update_available) 36 | autoUpdater.on("update-not-available", update_not_available) 37 | autoUpdater.on("update-downloaded", update_downloaded) 38 | autoUpdater.on("error", update_error) 39 | 40 | exports.update_available = update_available 41 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | YT Downloader 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /back/requests.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios") 2 | const https = require("https") 3 | const qs = require("querystring") 4 | const fs = require("fs") 5 | 6 | const http_instance = axios.create({ 7 | httpsAgent: new https.Agent( 8 | { 9 | rejectUnauthorized: true 10 | } 11 | ) 12 | }) 13 | 14 | 15 | function getRequest(url, data){ 16 | return new Promise((resolve, reject) => { 17 | http_instance.get(url, { 18 | params: data 19 | }).then(resp => { 20 | resolve(resp) 21 | }).catch(err => { 22 | reject(err) 23 | }) 24 | }) 25 | } 26 | function postRequest(url, data){ 27 | return new Promise((resolve, reject) => { 28 | http_instance.post(url, qs.stringify(data)).then(resp => { 29 | resolve(resp) 30 | }).catch(err => { 31 | reject(err) 32 | }) 33 | }) 34 | } 35 | function downloadRequest(url, target, progressCallback){ 36 | return new Promise(async (resolve, reject) => { 37 | http_instance.get(url, { 38 | responseType: "stream", 39 | onDownloadProgress: progressCallback 40 | }).then(resp => { 41 | let writer = fs.createWriteStream(target) 42 | resp.data.pipe(writer) 43 | writer.on("finish", resolve) 44 | writer.on("error", reject) 45 | }) 46 | }) 47 | } 48 | 49 | module.exports = { 50 | getRequest, 51 | postRequest, 52 | downloadRequest 53 | } 54 | -------------------------------------------------------------------------------- /static/js/effects.js: -------------------------------------------------------------------------------- 1 | function createRipple(elem) { 2 | if(elem.classList.contains("ripple-legacy")) return 3 | 4 | elem.classList.add("mad-ripple") 5 | elem.addEventListener("mousedown", e => { 6 | if(elem.classList.contains(".disabled")) { 7 | return 8 | } 9 | 10 | function getElementOffset(element){ 11 | let de = document.documentElement 12 | let box = element.getBoundingClientRect() 13 | let top = box.top + window.pageYOffset - de.clientTop 14 | let left = box.left + window.pageXOffset - de.clientLeft 15 | return { top, left } 16 | } 17 | 18 | let offs = getElementOffset(elem) 19 | let x = e.pageX - offs.left 20 | let y = e.pageY - offs.top 21 | let dia = Math.min(elem.offsetHeight, elem.offsetWidth, 100) 22 | let ripple = document.createElement("div") 23 | ripple.classList.add("ripple-inner") 24 | elem.append(ripple) 25 | 26 | let rippleWave = document.createElement("div") 27 | rippleWave.classList.add("rippleWave") 28 | rippleWave.style.left = (x - dia/2).toString() + "px" 29 | rippleWave.style.top = (y - dia/2).toString() + "px" 30 | rippleWave.style.width = dia.toString() + "px" 31 | rippleWave.style.height = dia.toString() + "px" 32 | 33 | ripple.append(rippleWave) 34 | rippleWave.addEventListener("animationend", _ => { 35 | ripple.remove() 36 | }) 37 | }) 38 | } 39 | 40 | { 41 | document.querySelectorAll(".btn, .dropdown-item, .card-header, .ripple, .mad-ripple").forEach(elem => { 42 | elem.classList.add("ripple") 43 | createRipple(elem) 44 | }) 45 | document.addEventListener("click", _ => { 46 | document.querySelectorAll(".btn:not(.ripple), .dropdown-item:not(.ripple), .card-header:not(.ripple)").forEach(elem => { 47 | elem.classList.add("ripple") 48 | createRipple(elem) 49 | }) 50 | }) 51 | } -------------------------------------------------------------------------------- /static/bootstrap/fonts/lato/lato.css: -------------------------------------------------------------------------------- 1 | /* latin-ext */ 2 | @font-face { 3 | font-family: 'Lato'; 4 | font-style: italic; 5 | font-weight: 400; 6 | font-display: swap; 7 | src: url(./S6u8w4BMUTPHjxsAUi-qJCY.woff2) format('woff2'); 8 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 9 | } 10 | /* latin */ 11 | @font-face { 12 | font-family: 'Lato'; 13 | font-style: italic; 14 | font-weight: 400; 15 | font-display: swap; 16 | src: url(./S6u8w4BMUTPHjxsAXC-q.woff2) format('woff2'); 17 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 18 | } 19 | /* latin-ext */ 20 | @font-face { 21 | font-family: 'Lato'; 22 | font-style: normal; 23 | font-weight: 400; 24 | font-display: swap; 25 | src: url(./S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2'); 26 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 27 | } 28 | /* latin */ 29 | @font-face { 30 | font-family: 'Lato'; 31 | font-style: normal; 32 | font-weight: 400; 33 | font-display: swap; 34 | src: url(./S6uyw4BMUTPHjx4wXg.woff2) format('woff2'); 35 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 36 | } 37 | /* latin-ext */ 38 | @font-face { 39 | font-family: 'Lato'; 40 | font-style: normal; 41 | font-weight: 700; 42 | font-display: swap; 43 | src: url(./S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2'); 44 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 45 | } 46 | /* latin */ 47 | @font-face { 48 | font-family: 'Lato'; 49 | font-style: normal; 50 | font-weight: 700; 51 | font-display: swap; 52 | src: url(./S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2'); 53 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 54 | } -------------------------------------------------------------------------------- /static/css/effects.css: -------------------------------------------------------------------------------- 1 | /*Button ripple animation*/ 2 | .mad-ripple { 3 | position: relative; 4 | } 5 | .ripple-inner { 6 | position: absolute; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | overflow: hidden; 12 | pointer-events: none; 13 | /* allow user interaction */ 14 | border-radius: inherit; 15 | /* inherit from parent (rounded buttons etc) */ 16 | transform: translateZ(0); 17 | animation: ripple-shadow 0.4s forwards; 18 | } 19 | .rippleWave { 20 | position: absolute; 21 | background: rgba(255, 255, 255, .6); 22 | backface-visibility: hidden; 23 | border-radius: 50%; 24 | opacity: 0.45; 25 | transform: scale(0.7); 26 | animation: ripple 1s forwards; 27 | } 28 | @keyframes ripple-shadow { 29 | 0% { 30 | box-shadow: 0 0 0 rgba(0, 0, 0, 0); 31 | } 32 | 20% { 33 | box-shadow: 0 4px 16px rgba(0, 0, 0, .3); 34 | } 35 | 100% { 36 | box-shadow: 0 0 0 rgba(0, 0, 0, 0); 37 | } 38 | } 39 | @keyframes ripple { 40 | to { 41 | opacity: 0; 42 | transform: scale(24); 43 | } 44 | } 45 | 46 | 47 | /*Loading animation*/ 48 | .page-loader { 49 | position: relative; 50 | text-align: center; 51 | width: 100%; 52 | height: 100%; 53 | padding-top: 35vh; 54 | } 55 | .page-loader .text{ 56 | margin-top: 20px; 57 | } 58 | .page-loader .box { 59 | display: inline-block; 60 | width: 50px; 61 | height: 50px; 62 | background: #fff; 63 | animation: page-loader-box-animate .5s linear infinite; 64 | border-radius: 3px; 65 | } 66 | @keyframes page-loader-box-animate { 67 | 17% { border-bottom-right-radius: 3px; } 68 | 25% { transform: translateY(9px) rotate(22.5deg); } 69 | 50% { 70 | transform: translateY(18px) scale(1,.9) rotate(45deg) ; 71 | border-bottom-right-radius: 40px; 72 | } 73 | 75% { transform: translateY(9px) rotate(67.5deg); } 74 | 100% { transform: translateY(0) rotate(90deg); } 75 | } 76 | .page-loader .shadow { 77 | position: absolute; 78 | display: inline-block; 79 | width: 50px; 80 | height: 5px; 81 | background: #000; 82 | opacity: 0.3; 83 | border-radius: 50%; 84 | animation: page-loader-shadow-animate .5s linear infinite; 85 | top: calc(35vh + 60px); 86 | } 87 | @keyframes page-loader-shadow-animate { 88 | 50% { 89 | transform: scale(1.2,1); 90 | } 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YouTube Video Downloader 2 | 3 | > Developed by [Davis_Software](https://github.com/Davis-Software) © 2022 4 | 5 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/Davis-Software/YTDownloader?style=for-the-badge) 6 | ![GitHub issues](https://img.shields.io/github/issues-raw/Davis-Software/YTDownloader?style=for-the-badge) 7 | ![GitHub closed issues](https://img.shields.io/github/issues-closed/Davis-Software/YTDownloader?style=for-the-badge) 8 | ![GitHub all releases](https://img.shields.io/github/downloads/Davis-Software/YTDownloader/total?style=for-the-badge) 9 | ![GitHub](https://img.shields.io/github/license/Davis-Software/YTDownloader?style=for-the-badge) 10 | 11 | > The Recode: Now with a new awesome looking UI design! 12 | > 13 | > Video info preview and easy/automatic format selection 14 | > ![](.github/img/preview1.png) 15 | > 16 | > Optimized for audio downloads with ability to add custom metadata and a thumbnail 17 | > ![](.github/img/preview2.png) 18 | > 19 | > #### And best of all... many UI animations and ripple effects! 20 | 21 | # Features 22 | - Playlist support (currently in beta) 23 | - High Quality Download 24 | - Applying metadata and thumbnails to output file 25 | - Output converter 26 | - Preview video information (thumbnail, title, description, views, etc...) 27 | - Compatible with all [youtube-dl](https://github.com/ytdl-org/youtube-dl) websites (not only YouTube) 28 | - Automatic download / update of [youtube-dl](https://github.com/ytdl-org/youtube-dl) and [FFMpeg](https://ffmpeg.org/) 29 | 30 | # Requirements 31 | * #### Releases 32 | * `tar` is required for extracting the `ffmpeg` archive on Linux 33 | * all other requirements are included in the installer or downloaded automatically 34 | * #### Compiling 35 | * `git` is required for cloning the repository 36 | * `nodejs` and `npm` are required for compiling the application 37 | 38 | # Installation 39 | * Download an installer 40 | * Go to the [releases](https://github.com/Davis-Software/YTDownloader/releases) page and download an installer compatible with your OS 41 | * Compile yourself: 42 | * Clone the repository `git clone https://github.com/Davis-Software/YTDownloader.git` 43 | * Enter directory `cd YTDownloader-master` 44 | * Install required packages `npm install` 45 | * Run the application to check if it's working `npm start` 46 | * Compile the application into an installer `npm run dist` (for win64) 47 | * Consult `package.json` for more commands & info 48 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require("electron") 2 | 3 | 4 | contextBridge.exposeInMainWorld("utils", { 5 | path: { 6 | join: (...args) => args.join("/") 7 | }, 8 | openExternal: (url) => ipcRenderer.invoke("openExternal", url) 9 | }) 10 | contextBridge.exposeInMainWorld("ipc", { 11 | debug: {on: (channel, listener) => { 12 | ipcRenderer.on(`debug:${channel}`, listener) 13 | }}, 14 | update: {onInfo: listener => { 15 | ipcRenderer.on("update:info", listener) 16 | }} 17 | }) 18 | contextBridge.exposeInMainWorld("controls", { 19 | minimize: callback => { 20 | ipcRenderer.invoke("window:minimize").then(callback) 21 | }, 22 | close: callback => { 23 | ipcRenderer.invoke("window:close").then(callback) 24 | }, 25 | setProgressBar: value => { 26 | ipcRenderer.invoke("window:setProgressBar", value).then() 27 | }, 28 | flashFrame: value => { 29 | ipcRenderer.invoke("window:flashFrame", value).then() 30 | } 31 | }) 32 | contextBridge.exposeInMainWorld("dialog", { 33 | on: (channel, listener) => { 34 | ipcRenderer.on(`dialog:${channel}`, listener) 35 | }, 36 | showDialog: (options, resp, callback=_ => {}) => { 37 | let responder = `dialog:showDialogResponse:${resp}` 38 | ipcRenderer.invoke("dialog:showDialog", responder, options).then(_ => {callback(responder)}) 39 | } 40 | }) 41 | contextBridge.exposeInMainWorld("downloader", { 42 | on: (channel, listener) => { 43 | ipcRenderer.on(`downloader:${channel}`, listener) 44 | }, 45 | getInfo: (url, callback=_ => {}) => { 46 | let responder = "downloader:returnInfo" 47 | ipcRenderer.invoke("downloader:getInfo", responder, url).then(_ => {callback(responder)}) 48 | }, 49 | startDownload: (playlist, url, format, container, target, fileType, metadata, thumbnail) => { 50 | ipcRenderer.invoke("downloader:startDownload", playlist, url, format, container, target, fileType, metadata, thumbnail).then() 51 | }, 52 | killDownload: _ => { 53 | ipcRenderer.invoke("downloader:kill").then() 54 | } 55 | }) 56 | contextBridge.exposeInMainWorld("progressBar", { 57 | on: (channel, listener) => { 58 | ipcRenderer.on(`progress-bar:${channel}`, listener) 59 | }, 60 | create: (title, max, min, options = {}) => 61 | ipcRenderer.invoke("progress-bar:create", title, max, min, options), 62 | provide: (id, call, value) => 63 | ipcRenderer.invoke(`progress-bar:${call}:on-${id}`, value) 64 | }) 65 | -------------------------------------------------------------------------------- /static/css/index.css: -------------------------------------------------------------------------------- 1 | *{ 2 | -webkit-user-drag: none; 3 | user-select: none; 4 | } 5 | 6 | body{ 7 | margin-top: 32px; 8 | height: calc(100vh - 32px); 9 | background-image: url("../img/layered-waves-haikei.svg"); 10 | overflow: hidden; 11 | } 12 | 13 | #window-frame{ 14 | top: 0; 15 | display: block; 16 | position: fixed; 17 | height: 32px; 18 | width: 100%; 19 | background: #333333; 20 | z-index: 2048; 21 | } 22 | #window-frame .frame-logo img{ 23 | height: 20px; 24 | margin-top: 6px; 25 | margin-left: 6px; 26 | } 27 | #window-frame .frame-logo span{ 28 | position: absolute; 29 | width: max-content; 30 | margin-top: 6px; 31 | margin-left: 6px; 32 | font-size: 14px; 33 | font-weight: 10; 34 | } 35 | #window-frame .frame-drag-section{ 36 | position: fixed; 37 | top: 0; 38 | width: 100%; 39 | height: 32px; 40 | -webkit-app-region: drag; 41 | } 42 | #window-frame .frame-controls{ 43 | display: grid; 44 | grid-template-columns: repeat(2, 46px); 45 | position: absolute; 46 | top: 0; 47 | right: 0; 48 | -webkit-app-region: no-drag; 49 | } 50 | #window-frame .frame-btn{ 51 | grid-row: 1 / span 1; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | width: 100%; 56 | height: 32px; 57 | transition: all .15s; 58 | } 59 | #window-frame .frame-btn .icon { 60 | width: 11px; 61 | height: 11px; 62 | } 63 | #window-frame .frame-btn:hover{ 64 | background: #414141; 65 | cursor: pointer; 66 | } 67 | #window-frame .frame-btn:active{ 68 | background: #545454; 69 | } 70 | #window-frame .frame-btn.close_ico:hover{ 71 | background: #E81123 !important; 72 | } 73 | #window-frame .frame-btn.close_ico:active{ 74 | background: #F1707A !important; 75 | } 76 | 77 | 78 | .window-drag{ 79 | -webkit-app-region: drag; 80 | } 81 | .window-no-drag{ 82 | -webkit-app-region: no-drag; 83 | } 84 | 85 | 86 | .icon{ 87 | height: 40px; 88 | } 89 | .icon.i-2{ 90 | height: 80px; 91 | } 92 | .icon.round{ 93 | border-radius: 25px; 94 | } 95 | 96 | 97 | ::-webkit-scrollbar { 98 | width: 4px; 99 | height: 4px; 100 | } 101 | ::-webkit-scrollbar-track { 102 | background: rgb(102, 102, 102); 103 | } 104 | ::-webkit-scrollbar-thumb { 105 | background: #888; 106 | } 107 | ::-webkit-scrollbar-thumb:hover { 108 | background: #555; 109 | } 110 | ::selection { 111 | background: #3498db; 112 | } 113 | -------------------------------------------------------------------------------- /static/img/layered-waves-haikei.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/utils/progress-bar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | progress bar 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "YT Downloader", 3 | "homepage": "https://software-city.org/", 4 | "bugs": { 5 | "url": "https://github.com/Davis-Software/YTDownloader/issues", 6 | "email": "support@software-city.org" 7 | }, 8 | "author": { 9 | "name": "Software City Team", 10 | "email": "support@software-city.org", 11 | "url": "https://github.com/Davis-Software/YTDownloader" 12 | }, 13 | "name": "swc_ytdownloader", 14 | "version": "0.1.42", 15 | "description": "Download and convert Videos from YouTube", 16 | "repository": "https://github.com/Davis-Software/YTDownloader", 17 | "license": "MIT", 18 | "main": "index.js", 19 | "scripts": { 20 | "start": "electron --trace-warnings .", 21 | "pack": "electron-builder --dir", 22 | "dist": "electron-builder --x64 --win", 23 | "distlinux": "electron-builder --x64 --linux", 24 | "distmacOS": "electron-builder --x64 --mac", 25 | "publish": "electron-builder --x64 --win -p always", 26 | "publishlinux": "electron-builder --x64 --linux -p always", 27 | "publishmacOS": "electron-builder --x64 --mac -p always" 28 | }, 29 | "devDependencies": { 30 | "electron": "^39.0.0", 31 | "electron-builder": "^26.0.12" 32 | }, 33 | "dependencies": { 34 | "axios": "^1.6.5", 35 | "electron-updater": "^6.6.2", 36 | "js-yaml": "^4.1.0", 37 | "jsdom": "^24.0.0", 38 | "node-stream-zip": "^1.15.0", 39 | "compare-versions": "^6.1.1", 40 | "stream-json": "^1.9.1" 41 | }, 42 | "build": { 43 | "appId": "org.software-city.projects.ytdownloader", 44 | "afterPack": "./afterPackHook.js", 45 | "extraFiles": [], 46 | "nsis": { 47 | "oneClick": false, 48 | "perMachine": false 49 | }, 50 | "win": { 51 | "target": "nsis", 52 | "icon": "./static/logo/512x512.ico", 53 | "publish": { 54 | "provider": "github", 55 | "publishAutoUpdate": true, 56 | "releaseType": "release" 57 | } 58 | }, 59 | "linux": { 60 | "target": ["deb", "rpm", "AppImage"], 61 | "icon": "./static/logo/512x512.png", 62 | "category": "Utility", 63 | "publish": { 64 | "provider": "github", 65 | "publishAutoUpdate": true, 66 | "releaseType": "release" 67 | } 68 | }, 69 | "mac": { 70 | "target": ["dmg", "pkg", "zip"], 71 | "icon": "./static/logo/512x512.icns", 72 | "darkModeSupport": true, 73 | "publish": { 74 | "provider": "github", 75 | "publishAutoUpdate": true, 76 | "releaseType": "release" 77 | } 78 | }, 79 | "protocols": { 80 | "name": "YT Downloader", 81 | "schemes": [ 82 | "swc_ytdownloader" 83 | ] 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /back/utility-windows.js: -------------------------------------------------------------------------------- 1 | const {BrowserWindow, Menu} = require("electron") 2 | const path = require("path"); 3 | const {registerIpcListener} = require("./ipc-handler"); 4 | 5 | 6 | class ProgressBar { 7 | constructor(title, max, min, readyCallback, options = {}) { 8 | let win = new BrowserWindow({ 9 | frame: false, 10 | width: options.width || 300, 11 | height: options.height || (options.logVisible ? 200 : 80), 12 | resizable: false, 13 | alwaysOnTop: true, 14 | darkTheme: true, 15 | center: true, 16 | skipTaskbar: true, 17 | movable: true, 18 | webPreferences: { 19 | preload: path.join(__dirname, "../", "preload.js"), 20 | nodeIntegration: true 21 | } 22 | }) 23 | win.loadFile(path.join(__dirname, "../templates/utils", "progress-bar.html")).then() 24 | win.webContents.addListener("did-finish-load", () => { 25 | this.progress.webContents.send("progress-bar:set-title", title) 26 | win.webContents.send("progress-bar:set-max", max) 27 | win.webContents.send("progress-bar:set-min", min) 28 | readyCallback(this) 29 | }) 30 | win.identifier = "progress-bar" 31 | this.progress = win 32 | this.logVisible = options.logVisible || false 33 | } 34 | setValue(value) { 35 | this.progress.webContents.send("progress-bar:set-progress", value) 36 | } 37 | setText(text) { 38 | this.progress.webContents.send("progress-bar:set-text", text) 39 | } 40 | setInfo(info) { 41 | this.progress.webContents.send("progress-bar:set-info", info) 42 | } 43 | striped(bool = true) { 44 | this.progress.webContents.send("progress-bar:set-striped", bool) 45 | } 46 | removeAnimation() { 47 | this.progress.webContents.send("progress-bar:remove-animation") 48 | } 49 | log(level, message) { 50 | if(!this.logVisible) return 51 | this.progress.webContents.send("progress-bar:log", level, message) 52 | } 53 | close() { 54 | this.progress.close() 55 | } 56 | } 57 | 58 | 59 | function contextMenu(x, y, template) { 60 | return new Promise((resolve) => { 61 | template.forEach(item => { 62 | if(item.type === "separator") return 63 | item.click = () => {resolve(item.id)} 64 | }) 65 | 66 | Menu.buildFromTemplate(template).popup({x, y, callback: () => {resolve(undefined)}}) 67 | }) 68 | } 69 | 70 | 71 | registerIpcListener("progress-bar:create", async (event, title, max, min, options) => { 72 | async function make(){ 73 | return new Promise((resolve) => { 74 | let prBar = new ProgressBar(title, max, min, () => { 75 | let id = Math.round(Math.random()*1000000).toString() 76 | for(let method of ["setValue", "setText", "setInfo", "close"]){ 77 | registerIpcListener(`progress-bar:${method}:on-${id}`, (event, ...args) => { 78 | prBar[method](...args) 79 | return true 80 | }) 81 | } 82 | resolve(id) 83 | }, options) 84 | }) 85 | } 86 | return await make() 87 | }) 88 | 89 | registerIpcListener("context-menu:open", async (event, data) => { 90 | return await contextMenu(data.x, data.y, data.template) 91 | }) 92 | 93 | module.exports = {ProgressBar} 94 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | function loadPage(page){ 2 | fetch(`./views/${page}.html`).then(resp => { 3 | if(!resp.ok) return 4 | resp.text().then(data => { 5 | let main = document.querySelector("#main") 6 | let container = document.createElement("div") 7 | container.innerHTML = data 8 | 9 | container.querySelectorAll("script").forEach(elem => { 10 | let script = document.createElement("script") 11 | if(elem.hasAttribute("defer")){ 12 | script.setAttribute("defer", "") 13 | } 14 | if(elem.hasAttribute("src")){ 15 | script.setAttribute("src", elem.getAttribute("src")) 16 | } 17 | script.innerHTML = elem.innerHTML 18 | 19 | script.setAttribute("data-loaded-from", page) 20 | document.head.append(script) 21 | elem.remove() 22 | }) 23 | container.querySelectorAll("link").forEach(elem => { 24 | let link = document.createElement("link") 25 | if(elem.getAttribute("rel") !== "stylesheet") return 26 | 27 | link.setAttribute("href", elem.getAttribute("href")) 28 | link.setAttribute("rel", "stylesheet") 29 | 30 | link.setAttribute("data-loaded-from", page) 31 | document.head.append(link) 32 | elem.remove() 33 | }) 34 | 35 | main.innerHTML = container.innerHTML 36 | 37 | document.head.querySelectorAll("script, link").forEach(elem => { 38 | if(elem.hasAttribute("data-loaded-from") && elem.getAttribute("data-loaded-from") !== page){ 39 | elem.remove() 40 | } 41 | }) 42 | }) 43 | }) 44 | } 45 | 46 | 47 | (function (){ 48 | 49 | // Window frame controls 50 | { 51 | let minimizeBtn = document.querySelector(".frame-btn[data-action=min]") 52 | let closeBtn = document.querySelector(".frame-btn[data-action=close]") 53 | 54 | minimizeBtn.addEventListener("click", _ => { 55 | window.controls.minimize() 56 | }) 57 | closeBtn.addEventListener("click", _ => { 58 | window.controls.close() 59 | }) 60 | } 61 | 62 | // listener stuff 63 | window.ipc.update.onInfo((_, info) => { 64 | let modal = new Modal(null, { 65 | title: `New Update Available: Version ${info.version}`, 66 | close_button: { 67 | type: "info", 68 | text: "OK" 69 | }, 70 | centered: true, 71 | scrollable: true 72 | }, "large") 73 | modal.Custom(info.releaseNotes).querySelectorAll("a").forEach(link => { 74 | link.addEventListener("click", e => { 75 | e.preventDefault() 76 | window.open(link.href, "_blank") 77 | }) 78 | }) 79 | modal.Text( 80 | "auto-update-info-text", 81 | "span", 82 | "The update will be downloaded in the background automatically!", 83 | "text-info position-absolute start-0 ps-3", 84 | {}, 85 | true 86 | ) 87 | modal.show() 88 | }) 89 | 90 | // General init 91 | if(window.hasOwnProperty("identifier") && window.identifier === "dialog"){ 92 | if(!window.hasOwnProperty("dialogType")) return 93 | loadPage(window.dialogType) 94 | }else{ 95 | loadPage("downloader") 96 | } 97 | })() 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, dialog, ipcMain, shell } = require("electron") 2 | const { autoUpdater } = require('electron-updater') 3 | const config = require("./back/config") 4 | 5 | const { YoutubeDlPackage, FfmpegPackage } = require("./back/dependency-installer") 6 | const downloader = require("./back/downloader") 7 | const path = require("path"); 8 | 9 | 10 | if(config.devMode){ 11 | app.setAppUserModelId(process.execPath) 12 | } 13 | app.allowRendererProcessReuse = true 14 | 15 | 16 | let win 17 | 18 | 19 | async function startMain(){ 20 | if(app.requestSingleInstanceLock()){ 21 | MainWindow() 22 | } 23 | } 24 | function MainWindow () { 25 | win = new BrowserWindow({ 26 | width: 1200, 27 | height: 720, 28 | resizable: false, 29 | darkTheme: true, 30 | frame: config.devMode, 31 | webPreferences: { 32 | preload: path.join(__dirname, "preload.js") 33 | } 34 | }) 35 | win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { 36 | console.log('Denied access to "' + permission + '" permission requested by site') 37 | return callback(false) 38 | }) 39 | 40 | win.identifier = "main-window" 41 | require("./back/update-service") 42 | if(!config.devMode && config.autoUpdate) { 43 | autoUpdater.checkForUpdates().then() 44 | } 45 | 46 | ipcMain.handle("window:minimize", _ => { 47 | win.minimize() 48 | }) 49 | ipcMain.handle("window:close", _ => { 50 | win.close() 51 | }) 52 | ipcMain.handle("window:setProgressBar", (_, value) => { 53 | win.setProgressBar(value) 54 | }) 55 | ipcMain.handle("window:flashFrame", (_, value) => { 56 | win.flashFrame(value) 57 | }) 58 | 59 | ipcMain.handle("openExternal", (_, url) => { 60 | shell.openExternal(url) 61 | }) 62 | 63 | ipcMain.handle("dialog:showDialog", (event, responder, options) => { 64 | dialog.showOpenDialog(win, options).then((result) => { 65 | win.webContents.send(responder, result) 66 | }).catch(err => { 67 | win.webContents.send("downloader:error", err) 68 | }) 69 | }) 70 | 71 | YoutubeDlPackage.checkForUpdate(true).then(ret => { 72 | console.log(ret ? "yt-dlp update successful" : "yt-dlp is up to date") 73 | }) 74 | FfmpegPackage.checkForUpdate(true).then(ret => { 75 | console.log(ret ? "ffmpeg update successful" : "ffmpeg is up to date") 76 | }) 77 | 78 | downloader.registerListeners() 79 | 80 | win.on('focus', () => win.flashFrame(false)) 81 | win.setIcon(config.iconPath) 82 | if(!config.devMode){ 83 | win.setMenu(null) 84 | } 85 | 86 | win.loadFile(path.join(__dirname, "templates", "index.html")).then() 87 | } 88 | app.whenReady().then(startMain) 89 | app.on('activate', () => { 90 | if (BrowserWindow.getAllWindows().length === 0) { 91 | MainWindow() 92 | } 93 | }) 94 | 95 | 96 | // Single Instance lock 97 | function openedByUrl(url) { 98 | if (url) { 99 | win.webContents.send('openedByUrl', url) 100 | } 101 | } 102 | if (app.requestSingleInstanceLock()) { 103 | app.on('second-instance', (e, argv) => { 104 | if (config.platform === 'win32') { 105 | openedByUrl(argv.find((arg) => arg.startsWith('swc_desktopapp:'))) 106 | } 107 | if (win) { 108 | if (win.isMinimized()) win.restore() 109 | win.show() 110 | win.focus() 111 | } 112 | } 113 | )} 114 | 115 | if (!app.isDefaultProtocolClient('swc_desktopapp')) { 116 | app.setAsDefaultProtocolClient('swc_desktopapp') 117 | } 118 | -------------------------------------------------------------------------------- /static/css/views/downloader.css: -------------------------------------------------------------------------------- 1 | #messagebox{ 2 | position: relative; 3 | background-color: #850000; 4 | margin: 1.5rem; 5 | padding: 5px; 6 | border-radius: .25rem; 7 | border: solid 2px #ff0000; 8 | } 9 | #messagebox button{ 10 | position: absolute; 11 | right: 5px; 12 | bottom: 5px; 13 | } 14 | 15 | #settings{ 16 | height: 80vh; 17 | background-color: #272b30; 18 | margin: 1.5rem; 19 | padding: 5px; 20 | border-radius: .25rem; 21 | } 22 | #settings .form-select{ 23 | background: #171d23; 24 | margin-top: 5px; 25 | padding: 8px; 26 | border-radius: .25rem; 27 | } 28 | 29 | #video-preview{ 30 | position: relative; 31 | height: 80vh; 32 | background-color: #272b30; 33 | margin: 1.5rem; 34 | padding: 5px; 35 | border-radius: .25rem; 36 | } 37 | #video-preview div.part{ 38 | width: 50%; 39 | height: 100%; 40 | padding: 5px; 41 | } 42 | #video-preview div.part:first-child{ 43 | float: left; 44 | } 45 | #video-preview div.part:last-child{ 46 | float: right; 47 | } 48 | 49 | #video-thumbnail{ 50 | width: 100%; 51 | height: 60%; 52 | background-color: #1a1c1f; 53 | object-fit: cover; 54 | border-radius: .25rem; 55 | } 56 | #video-title{ 57 | background-color: #1a1c1f; 58 | padding: 8px; 59 | font-weight: bold; 60 | margin-top: 5px; 61 | border-radius: .25rem; 62 | user-select: text; 63 | } 64 | #video-info{ 65 | padding: 8px; 66 | border-radius: .25rem; 67 | background-color: #171d23; 68 | } 69 | #video-info a, #video-info span{ 70 | user-select: text; 71 | } 72 | #video-description{ 73 | padding: 8px; 74 | border-radius: .25rem; 75 | background-color: #171d23; 76 | height: 50%; 77 | overflow-y: auto; 78 | user-select: text; 79 | } 80 | #video-description::-webkit-scrollbar { 81 | width: 4px; 82 | height: 4px; 83 | } 84 | #video-description::-webkit-scrollbar-track { 85 | background: #171d23; 86 | overflow: hidden; 87 | border-top-right-radius: .25rem; 88 | border-bottom-right-radius: .25rem; 89 | } 90 | #video-description::-webkit-scrollbar-thumb { 91 | background: #323f4d; 92 | border-radius: .25rem; 93 | } 94 | #video-description::-webkit-scrollbar-thumb:hover { 95 | background: #3a4857; 96 | } 97 | #video-categories{ 98 | margin-top: 5px; 99 | padding: 8px; 100 | border-radius: .25rem; 101 | background-color: #171d23; 102 | overflow-x: auto; 103 | } 104 | #video-categories::-webkit-scrollbar { 105 | width: 4px; 106 | height: 4px; 107 | } 108 | #video-categories::-webkit-scrollbar-track { 109 | background: #171d23; 110 | overflow: hidden; 111 | border-top-right-radius: .25rem; 112 | border-bottom-right-radius: .25rem; 113 | } 114 | #video-categories::-webkit-scrollbar-thumb { 115 | background: #323f4d; 116 | border-radius: .25rem; 117 | } 118 | #video-categories::-webkit-scrollbar-thumb:hover { 119 | background: #3a4857; 120 | } 121 | #video-categories span{ 122 | display: inline; 123 | margin: 0 2px; 124 | } 125 | #format-selector{ 126 | background: #171d23; 127 | margin-top: 5px; 128 | padding: 8px; 129 | border-radius: .25rem; 130 | } 131 | #video-preview-continue{ 132 | position: absolute; 133 | bottom: 20px; 134 | right: 20px; 135 | } 136 | 137 | #video-download{ 138 | position: relative; 139 | height: 80vh; 140 | margin: 1.5rem; 141 | padding: 5px; 142 | } 143 | #video-download .solid-label-width{ 144 | display: inline; 145 | width: 180px; 146 | text-align: center; 147 | } 148 | .expand-collapse{ 149 | height: 100%; 150 | } 151 | #download-options{ 152 | position: relative; 153 | border-radius: .25rem; 154 | background-color: #272b30; 155 | margin: 20px 10px 10px; 156 | padding: 10px 15px; 157 | height: 440px; 158 | } 159 | #download-options .form-control{ 160 | background: #171d23; 161 | } 162 | #download-options-thumbnail-preview{ 163 | width: 25%; 164 | border-radius: .25rem; 165 | object-fit: contain; 166 | object-position: top center; 167 | margin: 5px 10px 0; 168 | height: 120px; 169 | } 170 | #download-options #download-options-back{ 171 | position: absolute; 172 | left: 20px; 173 | bottom: 20px; 174 | } 175 | #download-options #download-options-start{ 176 | position: absolute; 177 | right: 20px; 178 | bottom: 20px; 179 | } 180 | 181 | #download-progress{ 182 | position: relative; 183 | border-radius: .25rem; 184 | background-color: #272b30; 185 | margin: 20px 10px 10px; 186 | padding: 10px 15px; 187 | } 188 | #download-log{ 189 | position: relative; 190 | border-radius: .25rem; 191 | background-color: #272b30; 192 | margin: 20px 10px 10px; 193 | padding: 10px 15px; 194 | height: 190px; 195 | user-select: text; 196 | } 197 | #download-log ul{ 198 | overflow: auto; 199 | height: 140px; 200 | } 201 | #download-log ul li.info{ 202 | color: var(--bs-info); 203 | } 204 | #download-log ul li.warn{ 205 | color: var(--bs-warning); 206 | } 207 | #download-log ul li.err{ 208 | color: var(--bs-danger); 209 | } 210 | 211 | 212 | .attention{ 213 | outline: 0 solid var(--bs-info); 214 | animation-name: attention; 215 | animation-duration: .5s; 216 | animation-iteration-count: 5; 217 | animation-timing-function: ease-in-out; 218 | } 219 | @keyframes attention { 220 | 0% { 221 | outline: 0 solid var(--bs-info); 222 | } 223 | 50% { 224 | outline: 5px solid var(--bs-info); 225 | } 226 | 100% { 227 | outline: 0 solid var(--bs-info); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /static/js/modules/modal-generator.js: -------------------------------------------------------------------------------- 1 | class Modal{ 2 | static slim_modal_body = ` 3 | 14 | ` 15 | static basic_modal_body = ` 16 | 28 | ` 29 | constructor(wrapper_selector, options={}, size="normal"){ 30 | if(!wrapper_selector){ 31 | this.root = document.createElement("div") 32 | document.body.appendChild(this.root) 33 | }else{ 34 | this.root = document.querySelector(wrapper_selector) 35 | } 36 | 37 | this.id = options.id || Math.random().toString(16).substr(2, 8) 38 | this.title = options.title || "Modal" 39 | this.close_button = options.close_button || false 40 | 41 | this.template = options.template || Modal.basic_modal_body 42 | this.centered = options.centered || false 43 | this.scrollable = options.scrollable || false 44 | this.static_backdrop = options.static_backdrop || false 45 | 46 | this.root.innerHTML = this.template 47 | .replaceAll("modal_id", this.id) 48 | .replaceAll("modal_title", this.title) 49 | 50 | this.wrapper = document.getElementById(this.id) 51 | this.wrapper_config = this.wrapper.querySelector(".modal-dialog") 52 | this.wrapper_body = this.wrapper.querySelector(".modal-body") 53 | this.wrapper_footer = this.wrapper.querySelector(".modal-footer") 54 | 55 | if(this.close_button){ 56 | let btn = document.createElement("button") 57 | btn.setAttribute("class", "btn btn-danger") 58 | btn.setAttribute("data-bs-dismiss", "modal") 59 | if(typeof this.close_button === "string") { 60 | btn.innerHTML = this.close_button 61 | } 62 | else if(typeof this.close_button === "object"){ 63 | btn.setAttribute("class", `btn btn-${this.close_button.type}`) 64 | btn.innerHTML = this.close_button.text 65 | }else{ 66 | btn.innerText = "Close" 67 | } 68 | this.wrapper_footer.appendChild(btn) 69 | this.close_button = btn 70 | } 71 | 72 | if(this.centered){ 73 | this.wrapper_config.classList.add("modal-dialog-centered") 74 | } 75 | 76 | if(this.scrollable){ 77 | this.wrapper_config.classList.add("modal-dialog-scrollable") 78 | } 79 | 80 | if(this.static_backdrop){ 81 | this.wrapper.setAttribute("data-bs-backdrop", "static") 82 | this.wrapper.setAttribute("data-bs-keyboard", "false") 83 | } 84 | 85 | switch(size){ 86 | case "small": this.wrapper_config.classList.add("modal-sm"); break 87 | case "normal": break 88 | case "large": this.wrapper_config.classList.add("modal-lg"); break 89 | case "huge": this.wrapper_config.classList.add("modal-xl"); break 90 | case "max": this.wrapper_config.classList.add("modal-xxl"); break 91 | default: this.wrapper_config.classList.add(size); break 92 | } 93 | } 94 | SetOptions(options={}){ 95 | for(let option in options) { 96 | let value = options[option] 97 | switch (option) { 98 | case "title": { 99 | this.wrapper.querySelector(".modal-title").innerText = value 100 | break 101 | } 102 | default: { 103 | console.error("Provided invalid option") 104 | break 105 | } 106 | } 107 | } 108 | return this 109 | } 110 | 111 | _AppendElement(elem, footer){ 112 | if(footer){ 113 | this.wrapper_footer.appendChild(elem) 114 | }else{ 115 | this.wrapper_body.appendChild(elem) 116 | } 117 | } 118 | _CustomAttrs(elem, attrs){ 119 | for(let attr in attrs){ 120 | elem.setAttribute(attr, attrs[attr]) 121 | } 122 | } 123 | _GetInstance(){ 124 | return bootstrap.Modal.getOrCreateInstance(this.wrapper) 125 | } 126 | 127 | Button(id, text, classes="", attributes={}, footer=false){ 128 | let btn = document.createElement("button") 129 | btn.id = id 130 | btn.setAttribute("class", `btn ${classes}`) 131 | btn.innerHTML = text 132 | this._CustomAttrs(btn, attributes) 133 | this._AppendElement(btn, footer) 134 | return btn 135 | } 136 | Input(id, type, classes="", attributes= {}, footer=false){ 137 | let inp = document.createElement("input") 138 | inp.id = id 139 | inp.type = type 140 | inp.setAttribute("class", `form-control ${classes}`) 141 | this._CustomAttrs(inp, attributes) 142 | this._AppendElement(inp, footer) 143 | return inp 144 | } 145 | TextArea(id, classes="", attributes= {}){ 146 | let text_area = document.createElement("textarea") 147 | text_area.id = id 148 | text_area.setAttribute("class", `form-control ${classes}`) 149 | this._CustomAttrs(text_area, attributes) 150 | this.wrapper_body.appendChild(text_area) 151 | return text_area 152 | } 153 | Text(id, tag="span", text, classes="", attributes= {}, footer=false){ 154 | let span = document.createElement(tag) 155 | span.id = id 156 | span.innerText = text 157 | span.setAttribute("class", classes) 158 | this._CustomAttrs(span, attributes) 159 | this._AppendElement(span, footer) 160 | return span 161 | } 162 | FastText(text, attributes={}, footer=false){ 163 | let span = document.createElement("span") 164 | span.id = Math.random().toString() 165 | span.innerText = text 166 | this._CustomAttrs(span, attributes) 167 | this._AppendElement(span, footer) 168 | return span 169 | } 170 | Custom(html, tag="div", footer=false){ 171 | let elem = document.createElement(tag) 172 | elem.id = Math.random().toString() 173 | elem.innerHTML = html 174 | this._AppendElement(elem, footer) 175 | return elem 176 | } 177 | show(){ 178 | this._GetInstance().show() 179 | } 180 | on(event, callback){ 181 | this.wrapper.addEventListener(`${event}.bs.modal`, callback) 182 | } 183 | hide(){ 184 | this._GetInstance().hide() 185 | } 186 | clear(){ 187 | this.wrapper_body.innerHTML = "" 188 | this.wrapper_footer.innerHTML = "" 189 | return this 190 | } 191 | destroy(){ 192 | this.hide() 193 | // document.querySelectorAll(".modal-backdrop").forEach(e => { 194 | // document.removeChild(e) 195 | // }) 196 | // document.removeChild(this.wrapper) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /back/dependency-installer.js: -------------------------------------------------------------------------------- 1 | const { platform, appDataDir, tempDir } = require("./config") 2 | const requests = require("./requests") 3 | const fs = require("fs") 4 | const path = require("path") 5 | const { JSDOM } = require("jsdom") 6 | const StreamZip = require('node-stream-zip'); 7 | const {compare} = require("compare-versions"); 8 | const {ProgressBar} = require("./utility-windows"); 9 | const {exec} = require("child_process"); 10 | 11 | 12 | class Dependency{ 13 | constructor(id, target) { 14 | this.id = id 15 | this.target = target 16 | this.version = platform 17 | this.configFile = path.join(appDataDir, this.id) 18 | 19 | if(!fs.existsSync(this.configFile)){ 20 | fs.writeFileSync(this.configFile, JSON.stringify({ 21 | target: this.target, 22 | tag: null, 23 | platform 24 | }), { 25 | encoding: "utf-8" 26 | }) 27 | } 28 | } 29 | makeExecutable(filepath, callback){ 30 | fs.chmod(filepath, fs.constants.S_IRUSR | fs.constants.S_IWUSR | fs.constants.S_IXUSR, callback) 31 | } 32 | setConfigVal(key, val){ 33 | let conf = this.config 34 | conf[key] = val 35 | fs.writeFileSync(this.configFile, JSON.stringify(conf)) 36 | } 37 | get config(){ 38 | return JSON.parse(fs.readFileSync(this.configFile, { 39 | encoding: "utf-8" 40 | })) 41 | } 42 | checkAccess(){ 43 | if(!fs.existsSync(this.executor)) return false 44 | try { 45 | fs.accessSync(this.executor, fs.constants.X_OK | fs.constants.R_OK | fs.constants.W_OK) 46 | return true 47 | }catch (e){ 48 | console.error("Dependency Error: " + e.message) 49 | fs.rmSync(this.executor) 50 | return false 51 | } 52 | } 53 | 54 | /** 55 | * Implementation required 56 | * @param download 57 | * @returns {Promise} 58 | */ 59 | checkForUpdate(download){ 60 | throw new Error("Not Implemented") 61 | } 62 | 63 | /** 64 | * Implementation required 65 | * @returns {string} 66 | */ 67 | get executor(){ 68 | throw new Error("Not Implemented") 69 | } 70 | } 71 | 72 | class YoutubeDlDependency extends Dependency{ 73 | constructor(target) { 74 | super("yt_dl", target) 75 | 76 | this.url = "https://github.com/yt-dlp/yt-dlp/releases/latest" 77 | this.files = ["yt-dlp", "yt-dlp.exe"] 78 | } 79 | _getLatestTag(callback){ 80 | requests.getRequest(this.url).then(resp => { 81 | let document = new JSDOM(resp.data).window.document 82 | let tag = document.querySelector("li.breadcrumb-item-selected").textContent 83 | .replaceAll(" ", "") 84 | .replaceAll("\n", "") 85 | callback(tag) 86 | }) 87 | } 88 | checkForUpdate(download){ 89 | this.checkAccess() 90 | 91 | return new Promise(resolve => { 92 | let curr_tag = fs.existsSync(this.executor) ? this.config.tag : null 93 | this._getLatestTag(tag => { 94 | if(download && (curr_tag === null || compare(tag, curr_tag, ">"))){ 95 | let file = this.version === "unix" ? this.files[0] : this.files[1] 96 | let url = this.url.split("/") 97 | url.pop() 98 | url.push("download", tag, file) 99 | url = url.join("/") 100 | 101 | new ProgressBar("yt-dlp Installer", 100, 0, (bar) => { 102 | bar.setInfo("Downloading latest ffmpeg archive") 103 | 104 | function progressCallback(event){ 105 | let percent = Math.round((event.loaded / event.total) * 100) 106 | bar.setValue(percent) 107 | bar.setText(`${percent}% completed`) 108 | } 109 | 110 | requests.downloadRequest(url, path.join(this.target, file), progressCallback).then(_ => { 111 | bar.setValue(100) 112 | this.makeExecutable(path.join(this.target, file), () => { 113 | this.setConfigVal("tag", tag) 114 | bar.striped(false) 115 | bar.setText("yt-dlp update successful") 116 | bar.setInfo(`yt-dlp is now at version: ${tag}`) 117 | setTimeout(() => bar.close(), 2000) 118 | resolve(tag) 119 | }) 120 | }) 121 | }, {width: 500, height: 200, logVisible: false}) 122 | }else{ 123 | resolve(false) 124 | } 125 | }) 126 | }) 127 | } 128 | get executor(){ 129 | return path.join(this.target, this.version === "unix" ? this.files[0] : this.files[1]) 130 | } 131 | } 132 | 133 | class Ffmpeg extends Dependency{ 134 | constructor(target) { 135 | super("ffmpeg", target) 136 | 137 | this.url = "https://github.com/yt-dlp/FFmpeg-Builds/releases/latest" 138 | this.files = ["ffmpeg", "ffmpeg.exe"] 139 | this.url_files = ["ffmpeg-master-latest-linux64-gpl.tar.xz", "ffmpeg-master-latest-win64-gpl.zip"] 140 | } 141 | _getLatestTag(callback){ 142 | requests.getRequest(this.url).then(resp => { 143 | let document = new JSDOM(resp.data).window.document 144 | let tag = document.querySelector("div a code.f5").textContent 145 | .replaceAll(" ", "") 146 | .replaceAll("\n", "") 147 | callback(tag) 148 | }) 149 | } 150 | checkForUpdate(download){ 151 | this.checkAccess() 152 | 153 | const unzipWin = (downloadLoc, outLoc, callback) => { 154 | let zip = new StreamZip({ 155 | file: downloadLoc, 156 | storeEntries: true 157 | }) 158 | zip.on("ready", () => { 159 | for(let entry in zip.entries()){ 160 | if(entry.includes(this.files[1])){ 161 | zip.extract(entry, path.join(outLoc, this.files[1]), (err, _) => { 162 | if(err) return 163 | callback() 164 | zip.close() 165 | }) 166 | break 167 | } 168 | } 169 | }) 170 | } 171 | const unzipUnix = (downloadLoc, outLoc, callback) => { 172 | let baseName = path.basename(downloadLoc).split(".")[0] + "/bin/ffmpeg" 173 | exec(`tar --strip-components=2 -C "${outLoc}" -xf "${downloadLoc}" "${baseName}"`, (err, stdout, stderr) => { 174 | if(err) { 175 | console.error(err, stdout, stderr) 176 | return 177 | } 178 | callback() 179 | }) 180 | } 181 | const unzip = (downloadLoc, outLoc, callback) => { 182 | let extractor = this.version === "unix" ? unzipUnix : unzipWin 183 | extractor(downloadLoc, outLoc, callback) 184 | } 185 | 186 | return new Promise(resolve => { 187 | let curr_tag = fs.existsSync(this.executor) ? this.config.tag : null 188 | 189 | this._getLatestTag(tag => { 190 | if(download && (curr_tag === null || tag !== curr_tag)){ 191 | let file = this.version === "unix" ? this.url_files[0] : this.url_files[1] 192 | 193 | let temp_loc = path.join(tempDir, file) 194 | fs.mkdirSync(temp_loc, {recursive: true}) 195 | let downloadedLoc = path.join(temp_loc, file) 196 | 197 | let url = this.url.split("/") 198 | url.pop() 199 | url.push("download", "latest", file) 200 | url = url.join("/") 201 | 202 | new ProgressBar("ffmpeg Installer", 100, 0, (bar) => { 203 | bar.setInfo("Downloading latest ffmpeg archive") 204 | 205 | function progressCallback(event){ 206 | let percent = Math.round((event.loaded / event.total) * 100) 207 | bar.setValue(percent) 208 | bar.setText(`${percent}% completed`) 209 | } 210 | 211 | requests.downloadRequest(url, downloadedLoc, progressCallback).then(_ => { 212 | bar.setText("working...") 213 | bar.setInfo("Extracting ffmpeg archive") 214 | bar.setValue(100) 215 | bar.striped() 216 | unzip(downloadedLoc, this.target, () => { 217 | this.makeExecutable(path.join(this.target, file), () => { 218 | this.setConfigVal("tag", tag) 219 | bar.striped(false) 220 | bar.setText("ffmpeg update successful") 221 | bar.setInfo(`ffmpeg is now at version: ${tag}`) 222 | setTimeout(() => bar.close(), 2000) 223 | resolve(true) 224 | }) 225 | }) 226 | }) 227 | }, {width: 500, height: 200, logVisible: false}) 228 | }else{ 229 | resolve(false) 230 | } 231 | }) 232 | }) 233 | } 234 | get executor(){ 235 | return path.join(this.target, this.version === "unix" ? this.files[0] : this.files[1]) 236 | } 237 | } 238 | 239 | 240 | let installDir = path.join(appDataDir, "tools") 241 | fs.mkdirSync(installDir, { 242 | recursive: true 243 | }) 244 | 245 | module.exports = { 246 | YoutubeDlPackage: new YoutubeDlDependency(installDir), 247 | FfmpegPackage: new Ffmpeg(installDir) 248 | } 249 | -------------------------------------------------------------------------------- /templates/views/downloader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |

17 | 18 |
19 |
20 | 21 |
22 |
23 |

Welcome to the SWC YouTube Downloader

24 |
25 |

26 | Most of the supported websites of 'yt-dlp' are supported by this application.
27 | A list of supported websites can be found here. 28 |

29 |
30 | 31 |
32 |
Preset Settings
33 | 34 |
35 | 36 | 42 |
43 | 44 |
45 | 46 | 58 |
59 | 60 |
61 | 62 |
63 | 64 | 65 | 66 |
67 |
68 | 69 |
70 | 71 |
72 |
73 | 74 | 75 |
76 | 77 |
78 | 79 | 80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 |
Loading video info
93 |
94 |
95 | 96 |
97 |
98 |
99 | 100 |

Video title

101 |
102 |
103 | Creator:   
104 | Likes:   
105 | Views:    106 |
107 |
108 |
109 |
video description
110 |
111 |
112 |
113 | 114 | 126 |
127 | 128 | 132 | 133 | 137 |
138 |

139 |
140 | 141 | 142 |
143 |
144 |
145 | 146 |
147 |
148 |
149 | 150 | 162 |
163 |
164 | 165 | 166 | 167 |
168 | 169 |
170 |
171 |
172 |
173 |
174 | 175 | 176 |
177 |
178 | 179 |
180 |
181 |
182 |
183 | 184 | 185 |
186 |
187 | 188 | 189 |
190 | 191 | 192 |
193 |
194 |
195 |
196 | 197 |
198 | 199 |
200 | 201 | 202 |
203 |
204 |
205 | 206 |
207 |
208 | 209 | 210 |
211 | 212 |
213 |
214 |
215 | 216 | 217 | 218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | 227 |
228 |
229 |
Download Log
230 |
    231 |
    232 |
    233 |
    234 |
    235 | -------------------------------------------------------------------------------- /back/downloader.js: -------------------------------------------------------------------------------- 1 | const { YoutubeDlPackage, FfmpegPackage } = require("./dependency-installer") 2 | const { execFile } = require("child_process") 3 | const { registerIpcListener, invoke } = require("./ipc-handler") 4 | const { appDataDir } = require("./config") 5 | const { downloadRequest } = require("./requests") 6 | const path = require("path") 7 | const fs = require("fs") 8 | const {randomUUID} = require("crypto") 9 | 10 | const {parser} = require("stream-json"); 11 | const {streamObject} = require("stream-json/streamers/StreamObject") 12 | 13 | 14 | let currentProcess 15 | let aborted 16 | 17 | 18 | function log(msg, tag="prim"){ 19 | invoke("downloader:progress:log", { 20 | message: msg, 21 | tag 22 | }) 23 | } 24 | function makeError(err){ 25 | return{ 26 | shortMessage: err.shortMessage, 27 | command: err.command, 28 | escapedCommand: err.escapedCommand, 29 | exitCode: err.exitCode, 30 | signal: err.signal, 31 | signalDescription: err.signalDescription, 32 | stdout: err.stdout, 33 | stderr: err.stderr, 34 | failed: err.failed, 35 | timedOut: err.timedOut, 36 | isCanceled: err.isCanceled, 37 | killed: err.killed 38 | } 39 | } 40 | function killProcess(){ 41 | if(currentProcess){ 42 | currentProcess.kill() 43 | aborted = true 44 | } 45 | } 46 | 47 | 48 | class YoutubeDlVideo{ 49 | constructor(url) { 50 | this.uuid = randomUUID() 51 | this.url = url 52 | this.tempTarget = path.join(appDataDir, "targets") 53 | this.lastTarget = "" 54 | this.target = "" 55 | this._downloaded = false 56 | this.targetFormat = "" 57 | } 58 | getInfo(){ 59 | const ytInfo = execFile(YoutubeDlPackage.executor, [ 60 | "--dump-single-json", 61 | "--no-check-certificate", 62 | this.url 63 | ]) 64 | return new Promise((resolve, reject) => { 65 | const jsonResult = {} 66 | const pipeline = ytInfo.stdout.pipe(parser()).pipe(streamObject()) 67 | 68 | pipeline.on("data", ({ key, value }) => { 69 | jsonResult[key] = value; 70 | }); 71 | pipeline.on("end", () => { 72 | resolve(jsonResult); 73 | }); 74 | pipeline.on("error", reject); 75 | 76 | ytInfo.stderr.on("data", data => { 77 | console.warn(data) 78 | }) 79 | }) 80 | } 81 | download(format, container, target, fileType, callback){ 82 | log("Initialized downloader components...") 83 | log("Starting to download...") 84 | 85 | invoke("downloader:progress:info", "Starting download...") 86 | invoke("downloader:progress:mode", "stable") 87 | this.container = container 88 | this.target = target 89 | this.targetFormat = fileType 90 | this.lastTarget = `${this.uuid}-raw.${container}` 91 | 92 | const dwnOpts = [ 93 | "-f", format, 94 | "-o", path.join(this.tempTarget, this.lastTarget), 95 | "--ffmpeg-location", FfmpegPackage.executor, 96 | "--audio-quality", "0", 97 | this.url 98 | ] 99 | 100 | YoutubeDlVideo.ytDlProcess(dwnOpts, _ => { 101 | this._downloaded = true 102 | callback() 103 | }) 104 | } 105 | downloadPlaylist(format, target, fileType, embedMetadata, callback){ 106 | log("Initialized downloader components...") 107 | log("Starting to download...") 108 | 109 | invoke("downloader:progress:info", "Starting download...") 110 | invoke("downloader:progress:mode", "stable") 111 | this.target = target 112 | this.targetFormat = fileType 113 | 114 | fs.mkdirSync(path.join(this.tempTarget, this.uuid)) 115 | 116 | const dwnOpts = [ 117 | "-f", format, 118 | "-P", path.join(this.tempTarget, this.uuid), 119 | "--ffmpeg-location", FfmpegPackage.executor 120 | ] 121 | if(embedMetadata){ 122 | dwnOpts.push("--add-metadata") 123 | } 124 | dwnOpts.push(this.url) 125 | 126 | YoutubeDlVideo.ytDlProcess(dwnOpts, _ => { 127 | this._downloaded = true 128 | callback() 129 | }) 130 | } 131 | convertPlaylist(callback){ 132 | if(!this._downloaded) return 133 | 134 | log("Converting files to target format") 135 | invoke("downloader:progress:info", "Converting files...") 136 | invoke("downloader:progress:mode", "unstable") 137 | 138 | fs.mkdirSync(this.target, {recursive: true}) 139 | 140 | let filesL = fs.readdirSync(path.join(this.tempTarget, this.uuid)) 141 | let semaphoreL = 0 142 | 143 | function convertNext(basePath, semaphore, files, targetFormat, target, callback){ 144 | if(semaphore > 2 && files.length > 0){ 145 | setTimeout(() => { 146 | convertNext(basePath, semaphore, files, targetFormat, target, callback) 147 | }, 100) 148 | return 149 | }else if(files.length === 0){ 150 | callback() 151 | return 152 | } 153 | semaphore++ 154 | let file = files.pop() 155 | let convOptions = [ 156 | "-y", 157 | "-i", path.join(basePath, file), 158 | ] 159 | let name = `${file.split(".").slice(0, -1).join(".")}.${targetFormat}` 160 | convOptions.push(path.join(target, name)) 161 | YoutubeDlVideo.ffMpegProcess(convOptions, _ => { 162 | log(`Converted ${name}`) 163 | semaphore-- 164 | convertNext(basePath, semaphore, files, targetFormat, target, callback) 165 | }) 166 | } 167 | convertNext(path.join(this.tempTarget, this.uuid), semaphoreL, filesL, this.targetFormat, this.target, callback) 168 | } 169 | static ytDlProcess(options, callback){ 170 | if(process.platform === "darwin") options.push("--no-check-certificate") 171 | options.push("--ffmpeg-location", FfmpegPackage.executor) 172 | const ytDownload = execFile(YoutubeDlPackage.executor, options) 173 | currentProcess = ytDownload 174 | 175 | ytDownload.stdout.on("data", data => { 176 | log(data, "info") 177 | 178 | let output = data.trim().split(" ").filter(n => n) 179 | if (output[0] === '[download]' && parseFloat(output[1])){ 180 | invoke("downloader:progress:data", { 181 | progress: parseFloat(output[1]), 182 | size: output[3], 183 | speed: output[5], 184 | estimated: output[7], 185 | transferred: parseFloat(output[3]) * (parseFloat(output[1]) / 100) 186 | }) 187 | } 188 | }) 189 | ytDownload.stdout.on('close', _ => { 190 | if(aborted){ 191 | log("Download Aborted by user", "warn") 192 | invoke("downloader:progress:downloadAborted") 193 | aborted = false 194 | }else { 195 | log("Download process completed") 196 | callback() 197 | } 198 | }) 199 | ytDownload.stderr.on('data', data => { 200 | log(data, "err") 201 | }) 202 | } 203 | static ffMpegProcess(options, callback){ 204 | const convert = execFile(FfmpegPackage.executor, options) 205 | currentProcess = convert 206 | convert.addListener("error", err => { 207 | log(err, "warn") 208 | }) 209 | convert.addListener("close", _ => { 210 | if(aborted){ 211 | invoke("downloader:progress:downloadAborted") 212 | aborted = false 213 | }else{ 214 | callback() 215 | } 216 | }) 217 | } 218 | simpleConvert(callback){ 219 | if( 220 | (!this._downloaded || this.container === this.targetFormat) 221 | ) return callback() 222 | 223 | log("Converting file to target format") 224 | invoke("downloader:progress:info", "Converting file...") 225 | invoke("downloader:progress:mode", "unstable") 226 | 227 | let convTarget = String(this.lastTarget) 228 | this.lastTarget = `${this.uuid}-converted.${this.targetFormat}` 229 | let convOptions = [ 230 | "-y", 231 | "-i", path.join(this.tempTarget, convTarget), 232 | "-c", "copy", 233 | "-strict", "-2" 234 | ] 235 | convOptions.push(path.join(this.tempTarget, this.lastTarget)) 236 | YoutubeDlVideo.ffMpegProcess(convOptions, callback) 237 | } 238 | applyMetadata(data, callback){ 239 | if(!this._downloaded) return 240 | 241 | log("Applying metadata to file") 242 | invoke("downloader:progress:info", "Applying metadata...") 243 | invoke("downloader:progress:mode", "unstable") 244 | 245 | let convTarget = String(this.lastTarget) 246 | this.lastTarget = `${this.uuid}-metadata.` + this.targetFormat 247 | let options = [ 248 | "-y", 249 | "-i", path.join(this.tempTarget, convTarget), 250 | "-movflags", "use_metadata_tags", 251 | "-map_metadata", "0" 252 | ] 253 | for(let key in data){ 254 | options.push(...[ 255 | "-metadata", `${key}=${data[key]}` 256 | ]) 257 | } 258 | options.push(...[ 259 | // "-c", "copy", 260 | "-strict", "-2" 261 | ]) 262 | options.push(path.join(this.tempTarget, this.lastTarget)) 263 | YoutubeDlVideo.ffMpegProcess(options, callback) 264 | } 265 | applyThumbnail(thumbPath, callback){ 266 | if(!this._downloaded) return 267 | 268 | log("Applying thumbnail to file") 269 | invoke("downloader:progress:info", "Applying thumbnail...") 270 | invoke("downloader:progress:mode", "unstable") 271 | 272 | let metaTarget = String(this.lastTarget) 273 | this.lastTarget = `${this.uuid}-thumbnail.` + this.targetFormat 274 | let options = [ 275 | "-y", 276 | "-i", path.join(this.tempTarget, metaTarget), 277 | "-i", thumbPath, 278 | "-map", "0", 279 | "-map", "1", 280 | "-c:v:1", "png", 281 | "-disposition:v:1", "attached_pic", 282 | path.join(this.tempTarget, this.lastTarget) 283 | ] 284 | YoutubeDlVideo.ffMpegProcess(options, callback) 285 | } 286 | downloadThumbnail(url, callback){ 287 | log(`Downloading thumbnail from remote: ${url}`) 288 | invoke("downloader:progress:info", "Downloading thumbnail...") 289 | invoke("downloader:progress:mode", "unstable") 290 | downloadRequest(url, path.join(this.tempTarget, `${this.uuid}-thumb.png`)).then(_ => { 291 | log("Thumbnail download completed") 292 | this.applyThumbnail(path.join(this.tempTarget, `${this.uuid}-thumb.png`), callback) 293 | }) 294 | } 295 | copyToEndTarget(callback){ 296 | log("Copying file to target location") 297 | invoke("downloader:progress:info", "Copying to final location...") 298 | invoke("downloader:progress:mode", "unstable") 299 | fs.copyFile( 300 | path.join(this.tempTarget, this.lastTarget), 301 | this.target, 302 | callback 303 | ) 304 | } 305 | cleanup(){ 306 | log("All done - cleaning up temporary files") 307 | invoke("downloader:progress:info", "Done") 308 | invoke("downloader:progress:mode", "stable") 309 | 310 | if(!this._downloaded) return 311 | 312 | for(let file of fs.readdirSync(this.tempTarget)){ 313 | if(!file.startsWith(this.uuid)) continue 314 | fs.rm(path.join(this.tempTarget, file), {recursive: true}, () => {}) 315 | } 316 | 317 | invoke("downloader:progress:downloadComplete") 318 | } 319 | } 320 | 321 | function registerListeners() { 322 | registerIpcListener("downloader:getInfo", (_, responder, url) => { 323 | let video = new YoutubeDlVideo(url) 324 | video.getInfo().then(info => { 325 | invoke(responder, info) 326 | }).catch(err => { 327 | invoke("downloader:error", makeError(err)) 328 | console.error(err) 329 | }) 330 | }) 331 | registerIpcListener("downloader:startDownload", (_, playlist, url, format, container, target, fileType, metadata, thumbnail) => { 332 | let video = new YoutubeDlVideo(url) 333 | 334 | function end(){ 335 | video.copyToEndTarget(_ => { 336 | video.cleanup() 337 | }) 338 | } 339 | function applyThumbnail(){ 340 | if(thumbnail){ 341 | if(thumbnail.external){ 342 | video.downloadThumbnail(thumbnail.link, _ => { 343 | end() 344 | }) 345 | }else{ 346 | video.applyThumbnail(thumbnail.link, _ => { 347 | end() 348 | }) 349 | } 350 | }else{ 351 | end() 352 | } 353 | } 354 | function applyMetadata(){ 355 | if(metadata){ 356 | video.applyMetadata(metadata, _ => { 357 | applyThumbnail() 358 | }) 359 | }else{ 360 | applyThumbnail() 361 | } 362 | } 363 | function simpleConvert(){ 364 | if(metadata || thumbnail){ 365 | applyMetadata() 366 | }else{ 367 | video.simpleConvert(() => { 368 | end() 369 | }) 370 | } 371 | } 372 | 373 | if(playlist){ 374 | video.downloadPlaylist(format, target, fileType, !!metadata, () => { 375 | video.convertPlaylist(() => { 376 | video.cleanup() 377 | }) 378 | }) 379 | }else{ 380 | video.download(format, container, target, fileType, _ => { 381 | simpleConvert() 382 | }) 383 | } 384 | }) 385 | registerIpcListener("downloader:kill", _ => { 386 | killProcess() 387 | }) 388 | } 389 | 390 | module.exports = { 391 | YoutubeDlVideo, 392 | registerListeners 393 | } 394 | -------------------------------------------------------------------------------- /static/js/views/downloader.js: -------------------------------------------------------------------------------- 1 | const urlInput = document.querySelector("#dwn-url-input") 2 | const urlInputPaste = document.querySelector("#dwn-url-paste") 3 | const urlInputApply = document.querySelector("#dwn-url-apply") 4 | 5 | const messageBox = document.querySelector("#messagebox") 6 | const settings = document.querySelector("#settings") 7 | const loader = document.querySelector("#loader") 8 | const videoInfo = document.querySelector("#video-preview") 9 | const videoDownload = document.querySelector("#video-download") 10 | const videoFormatCustomBox = document.querySelector("#format-selector-box-custom") 11 | 12 | const messageBoxCollapse = new bootstrap.Collapse(messageBox.parentElement, {toggle: false}) 13 | const settingsCollapse = new bootstrap.Collapse(settings.parentElement) 14 | const loaderCollapse = new bootstrap.Collapse(loader.parentElement, {toggle: false}) 15 | const videoInfoCollapse = new bootstrap.Collapse(videoInfo.parentElement, {toggle: false}) 16 | const videoDownloadCollapse = new bootstrap.Collapse(videoDownload.parentElement, {toggle: false}) 17 | const videoFormatCustomCollapse = new bootstrap.Collapse(videoFormatCustomBox, {toggle: false}) 18 | 19 | const presetSettingsDownloadFormatSelect = settings.querySelector("#pr-setting-mode") 20 | const presetSettingsExportFormatSelect = settings.querySelector("#pr-setting-format") 21 | const presetSettingsDefaultFolderInput = settings.querySelector("#pr-setting-folder") 22 | const presetSettingsSongModeCheckbox = settings.querySelector("#pr-setting-metadata") 23 | const presetSettingsThumbnailCheckbox = settings.querySelector("#pr-setting-thumbnail") 24 | 25 | const videoFormatSelector = document.querySelector("#format-selector #format-selector-video") 26 | const videoFormatInfoBox = document.querySelector("#format-selector #format-info") 27 | const videoPreviewContinue = document.querySelector("#video-preview-continue") 28 | const videoFormatCustomVideoSelector = videoFormatCustomBox.querySelector("#format-selector-custom-video") 29 | const videoFormatCustomAudioSelector = videoFormatCustomBox.querySelector("#format-selector-custom-audio") 30 | const videoFileTypeSelector = document.querySelector("#output-filetype-select") 31 | const outputLocationInput = document.querySelector("#output-location-selector") 32 | 33 | const downloadOptionsCollapse = new bootstrap.Collapse( 34 | document.querySelector("#download-options").parentElement, 35 | {toggle: false} 36 | ) 37 | 38 | const downloadOptionsCustomFilenameCheckbox = document.querySelector("#download-options-custom-filename") 39 | const downloadOptionsCustomFilenameCollapse = new bootstrap.Collapse( 40 | downloadOptionsCustomFilenameCheckbox.parentElement.parentElement.querySelector(".collapse"), 41 | {toggle: false} 42 | ) 43 | const downloadOptionsCustomFilenameInput = document.querySelector("#download-options-custom-filename-input") 44 | const downloadOptionsSongModeCheckbox = document.querySelector("#download-options-songMode") 45 | const downloadOptionsSongModeCollapse = new bootstrap.Collapse( 46 | downloadOptionsSongModeCheckbox.parentElement.parentElement.querySelector(".collapse"), 47 | {toggle: false} 48 | ) 49 | const downloadOptionsSongModeArtistInput = document.querySelector("#download-options-songMode-artist") 50 | const downloadOptionsSongModeTitleInput = document.querySelector("#download-options-songMode-title") 51 | const downloadOptionsSongModeAffectsFileName = document.querySelector("#download-options-songMode-filename") 52 | 53 | const downloadOptionsApplyThumbnail = document.querySelector("#download-options-thumbnail") 54 | const downloadOptionsApplyThumbnailCollapse = new bootstrap.Collapse( 55 | downloadOptionsApplyThumbnail.parentElement.nextElementSibling, 56 | {toggle: false} 57 | ) 58 | const downloadOptionsApplyThumbnailPreview = document.querySelector("#download-options-thumbnail-preview") 59 | const downloadOptionsApplyThumbnailCustomCheckbox = document.querySelector("#download-options-custom-thumbnail") 60 | const downloadOptionsApplyThumbnailCustomInput = document.querySelector("#download-options-custom-thumbnail-input") 61 | 62 | const downloadOptionsBack = document.querySelector("#download-options-back") 63 | const downloadOptionsStart = document.querySelector("#download-options-start") 64 | 65 | const downloadProgress = document.querySelector("#download-progress") 66 | const downloadProgressCollapse = new bootstrap.Collapse(downloadProgress.parentElement, {toggle: false}) 67 | const downloadProgressBar = document.querySelector("#download-progress-bar .progress-bar") 68 | const downloadProgressInfo = document.querySelector("#download-progress-info") 69 | const downloadProgressAbortButton = document.querySelector("#download-abort") 70 | const downloadProgressLog = document.querySelector("#download-log ul") 71 | 72 | // windows RegEx for filename checking 73 | const fileNameFormat = /^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:";|/]+$/ 74 | 75 | let applied = false 76 | let videoInfoData 77 | 78 | let selectedDownloadFormat 79 | let selectedDownloadFormatContainer 80 | let selectedDownloadOutputMode 81 | let selectedDownloadOutputFileType 82 | let selectedDownloadOutputFilePath 83 | let selectedDownloadOutputFile 84 | 85 | let selectedThumbnail 86 | 87 | 88 | presetSettingsDownloadFormatSelect.value = localStorage.getItem("downloadFormat") || "none" 89 | presetSettingsExportFormatSelect.value = localStorage.getItem("exportFormat") || "none" 90 | presetSettingsDefaultFolderInput.value = localStorage.getItem("defaultFolder") || "" 91 | presetSettingsSongModeCheckbox.checked = localStorage.getItem("songMode") === "true" 92 | presetSettingsThumbnailCheckbox.checked = localStorage.getItem("thumbnail") === "true" 93 | 94 | presetSettingsDownloadFormatSelect.addEventListener("change", () => { 95 | localStorage.setItem("downloadFormat", presetSettingsDownloadFormatSelect.value) 96 | 97 | switch (presetSettingsDownloadFormatSelect.value){ 98 | case "preset-max-audio": 99 | presetSettingsExportFormatSelect.querySelector("optgroup[label='Audio']").disabled = false 100 | presetSettingsExportFormatSelect.querySelector("optgroup[label='Video']").disabled = true 101 | break 102 | case "preset-max": 103 | case "preset-max-video": 104 | presetSettingsExportFormatSelect.querySelector("optgroup[label='Audio']").disabled = true 105 | presetSettingsExportFormatSelect.querySelector("optgroup[label='Video']").disabled = false 106 | break 107 | default: 108 | presetSettingsExportFormatSelect.querySelector("optgroup[label='Audio']").disabled = false 109 | presetSettingsExportFormatSelect.querySelector("optgroup[label='Video']").disabled = false 110 | break 111 | } 112 | }) 113 | presetSettingsExportFormatSelect.addEventListener("change", () => { 114 | localStorage.setItem("exportFormat", presetSettingsExportFormatSelect.value) 115 | }) 116 | presetSettingsDefaultFolderInput.parentElement.querySelector(".btn-primary").addEventListener("click", () => { 117 | window.dialog.showDialog({ 118 | title: "Select a default output folder", 119 | buttonLabel: "Choose Folder", 120 | properties: ["openDirectory"] 121 | }, "selectDefaultLocation") 122 | }) 123 | window.dialog.on("showDialogResponse:selectDefaultLocation", (_, response) => { 124 | if(response.canceled) return 125 | presetSettingsDefaultFolderInput.value = response.filePaths[0] 126 | localStorage.setItem("defaultFolder", response.filePaths[0]) 127 | }) 128 | presetSettingsDefaultFolderInput.parentElement.querySelector(".btn-warning").addEventListener("click", () => { 129 | presetSettingsDefaultFolderInput.value = "" 130 | localStorage.setItem("defaultFolder", "") 131 | }) 132 | presetSettingsSongModeCheckbox.addEventListener("change", () => { 133 | localStorage.setItem("songMode", presetSettingsSongModeCheckbox.checked) 134 | }) 135 | presetSettingsThumbnailCheckbox.addEventListener("change", () => { 136 | localStorage.setItem("thumbnail", presetSettingsThumbnailCheckbox.checked) 137 | }) 138 | 139 | 140 | 141 | function resetEverything(ignoreMessageBox=false, showSettings=true){ 142 | applied = false 143 | 144 | urlInput.disabled = false 145 | urlInputPaste.disabled = false 146 | urlInputApply.classList.remove("btn-warning", "btn-info", "btn-success") 147 | urlInputApply.classList.add("btn-primary") 148 | urlInputApply.textContent = "Apply" 149 | urlInputApply.disabled = false 150 | 151 | if(!ignoreMessageBox) { 152 | messageBoxCollapse.hide() 153 | } 154 | loaderCollapse.hide() 155 | videoInfoCollapse.hide() 156 | videoDownloadCollapse.hide() 157 | videoFormatCustomCollapse.hide() 158 | videoFormatSelector.querySelector("option[value=custom]").disabled = false 159 | 160 | if(showSettings){ 161 | settingsCollapse.show() 162 | }else{ 163 | settingsCollapse.hide() 164 | } 165 | } 166 | function errorOut(title, message){ 167 | resetEverything(true, false) 168 | messageBox.querySelector("h5").textContent = title 169 | messageBox.querySelector("p").innerText = message 170 | 171 | window.controls.setProgressBar(0) 172 | window.controls.flashFrame(true) 173 | 174 | messageBoxCollapse.show() 175 | } 176 | messageBox.querySelector("button").addEventListener("click", _ => { 177 | messageBoxCollapse.hide() 178 | 179 | if(!loaderCollapse._isShown() && !videoInfoCollapse._isShown() && !videoDownloadCollapse._isShown()){ 180 | settingsCollapse.show() 181 | } 182 | }) 183 | 184 | videoInfo.querySelector("#video-creator").addEventListener("click", e => { 185 | e.preventDefault() 186 | window.utils.openExternal(videoInfoData.channel_url) 187 | }) 188 | 189 | 190 | function callAttention(elem){ 191 | document.querySelectorAll(".attention").forEach(e => e.classList.remove("attention")) 192 | elem.classList.add("attention") 193 | } 194 | 195 | 196 | function humanFileSize(bytes, dp=1) { 197 | let thresh = 1024 198 | if (Math.abs(bytes) < thresh) return bytes + ' B' 199 | let units = ['kB', 'MB', 'GB', 'TB'] 200 | let u = -1 201 | let r = 10**dp 202 | do { 203 | bytes /= thresh 204 | ++u 205 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1) 206 | return bytes.toFixed(dp) + ' ' + units[u] 207 | } 208 | function formatVideoFormat(formatObj){ 209 | return ` 210 | ${formatObj.format_note} 211 | -> ${formatObj.width}x${formatObj.height} 212 | [Audio: ${formatObj.acodec}] 213 | (${humanFileSize(formatObj.filesize)}) 214 | `.replaceAll("\n", "") 215 | } 216 | function formatAudioFormat(formatObj){ 217 | return ` 218 | audio only -> 219 | ${formatObj.acodec}-${formatObj.asr} 220 | (${humanFileSize(formatObj.filesize)}) 221 | `.replaceAll("\n", "") 222 | } 223 | 224 | 225 | urlInputPaste.addEventListener("click", _ => { 226 | navigator.clipboard.readText().then(result => { 227 | if(!result) return 228 | urlInput.value = result 229 | }) 230 | }) 231 | urlInputApply.addEventListener("click", _ => { 232 | if(urlInputApply.disabled) return 233 | if(applied){ 234 | resetEverything() 235 | applied = false 236 | }else{ 237 | if(urlInput.value === ""){ 238 | errorOut("Invalid Input Error", "Please provide a video url") 239 | return; 240 | } 241 | 242 | messageBoxCollapse.hide() 243 | settingsCollapse.hide() 244 | videoInfoCollapse.hide() 245 | videoDownloadCollapse.hide() 246 | 247 | applied = true 248 | urlInput.disabled = true 249 | urlInputPaste.disabled = true 250 | urlInputApply.classList.replace("btn-primary", "btn-info") 251 | urlInputApply.innerHTML = "
    " 252 | urlInputApply.disabled = true 253 | 254 | window.downloader.getInfo(urlInput.value, _ => { 255 | window.controls.setProgressBar(2) 256 | 257 | videoInfo.querySelector("img").src = "" 258 | videoInfo.querySelector("#video-title").textContent = "" 259 | videoInfo.querySelector("#video-creator").textContent = "" 260 | videoInfo.querySelector("#video-likes").textContent = "" 261 | videoInfo.querySelector("#video-views").textContent = "" 262 | videoInfo.querySelector("#video-description").innerText = "" 263 | videoFormatSelector.querySelector("option[value=custom]").disabled = false 264 | 265 | videoInfo.querySelectorAll("#video-categories span").forEach(elem => { 266 | elem.remove() 267 | }) 268 | videoFormatSelector.querySelectorAll("optgroup[label=All] option").forEach(elem => { 269 | elem.remove() 270 | }) 271 | videoFormatCustomVideoSelector.querySelectorAll("option[data-codeset]").forEach(elem => elem.remove()) 272 | videoFormatCustomAudioSelector.querySelectorAll("option[data-codeset]").forEach(elem => elem.remove()) 273 | videoFormatSelector.querySelector("option[disabled]").selected = true 274 | videoFormatCustomVideoSelector.querySelector("option[disabled]").selected = true 275 | videoFormatCustomAudioSelector.querySelector("option[disabled]").selected = true 276 | videoFormatInfoBox.textContent = "" 277 | videoPreviewContinue.disabled = true 278 | 279 | videoFileTypeSelector.querySelector("option[disabled]").selected = true 280 | outputLocationInput.value = "" 281 | outputLocationInput.parentElement.querySelector("button").disabled = true 282 | 283 | downloadOptionsCollapse.hide() 284 | downloadOptionsCustomFilenameCollapse.hide() 285 | downloadOptionsCustomFilenameCheckbox.checked = false 286 | downloadOptionsCustomFilenameInput.value = "" 287 | downloadOptionsSongModeCollapse.hide() 288 | downloadOptionsSongModeCheckbox.checked = false 289 | downloadOptionsSongModeArtistInput.value = "" 290 | downloadOptionsSongModeTitleInput.value = "" 291 | downloadOptionsSongModeAffectsFileName.checked = true 292 | 293 | downloadOptionsApplyThumbnail.checked = false 294 | downloadOptionsApplyThumbnailPreview.src = "" 295 | downloadOptionsApplyThumbnailCustomCheckbox.checked = false 296 | downloadOptionsApplyThumbnailCustomInput.disabled = true 297 | 298 | downloadProgressCollapse.hide() 299 | downloadProgressBar.style.width = "0" 300 | downloadProgressBar.textContent = "" 301 | downloadProgressBar.classList.remove("progress-bar-animated", "progress-bar-striped") 302 | downloadProgressInfo.textContent = "" 303 | 304 | videoFileTypeSelector.disabled = false 305 | outputLocationInput.parentElement.querySelector("button").disabled = false 306 | downloadProgressAbortButton.disabled = true 307 | 308 | loaderCollapse.show() 309 | }) 310 | } 311 | }) 312 | urlInput.addEventListener("keydown", e => { 313 | if(e.key !== "Enter") return 314 | urlInputApply.click() 315 | }) 316 | 317 | function isPlaylist(info = videoInfoData){ 318 | if(!info) return false 319 | return info.playlist_count && info.playlist_count > 1 320 | } 321 | window.downloader.on("returnInfo", (_, info) => { 322 | window.controls.setProgressBar(-1) 323 | 324 | if(!info){ 325 | errorOut("Info Error", "Failed to retrieve video info. Please check your url and try again.") 326 | return 327 | } 328 | 329 | function isYoutube(){ 330 | return info.webpage_url.includes("https://youtube.com/") 331 | } 332 | 333 | urlInputApply.textContent = "Change" 334 | urlInputApply.classList.replace("btn-info", "btn-warning") 335 | urlInputApply.disabled = false 336 | 337 | loaderCollapse.hide() 338 | videoInfoCollapse.show() 339 | 340 | if(isPlaylist(info)){ 341 | videoInfo.querySelector("img").src = info.thumbnails.pop()?.url 342 | videoInfo.querySelector("#video-description").innerText = `Videos in Playlist: \n${info.entries.map(entry => " " + entry.title).join("\n")}` 343 | 344 | let elem = document.createElement("option") 345 | elem.disabled = true 346 | elem.textContent = "Playlists don't support custom formats" 347 | videoFormatSelector.querySelector("optgroup[label=All]").append(elem) 348 | videoFormatSelector.querySelector("option[value=custom]").disabled = true 349 | 350 | selectedThumbnail = info.thumbnails.pop()?.url 351 | }else{ 352 | videoInfo.querySelector("img").src = info.thumbnail 353 | videoInfo.querySelector("#video-description").innerText = info.description || "No Video description" 354 | videoFormatSelector.querySelector("option[value=custom]").disabled = false 355 | 356 | for(let format of info.formats){ 357 | let elem = document.createElement("option") 358 | elem.value = format.format_id 359 | elem.textContent = format.width ? formatVideoFormat(format) : formatAudioFormat(format) 360 | videoFormatSelector.querySelector("optgroup[label=All]").append(elem) 361 | } 362 | for(let format of info.formats){ 363 | if(!format.width) continue; 364 | let elem = document.createElement("option") 365 | elem.setAttribute("data-codeset", "") 366 | elem.value = format.format_id 367 | elem.textContent = formatVideoFormat(format) 368 | videoFormatCustomVideoSelector.append(elem) 369 | } 370 | for(let format of info.formats){ 371 | if(format.width) continue; 372 | let elem = document.createElement("option") 373 | elem.setAttribute("data-codeset", "") 374 | elem.value = format.format_id 375 | elem.textContent = formatAudioFormat(format) 376 | videoFormatCustomAudioSelector.append(elem) 377 | } 378 | 379 | selectedThumbnail = info.thumbnail 380 | } 381 | 382 | videoInfo.querySelector("#video-title").textContent = info.title || "No title" 383 | 384 | videoInfo.querySelector("#video-creator").textContent = isYoutube() 385 | ? info.channel 386 | : (info.uploader || "Unknown") 387 | videoInfo.querySelector("#video-likes").textContent = info.like_count || "Unknown" 388 | videoInfo.querySelector("#video-views").textContent = info.view_count || "Unknown" 389 | 390 | for(let category of info.categories || ["No categories"]){ 391 | let elem = document.createElement("span") 392 | elem.classList.add("badge", "rounded-pill", "bg-secondary") 393 | elem.textContent = category 394 | videoInfo.querySelector("#video-categories").append(elem) 395 | } 396 | 397 | callAttention(videoFormatSelector) 398 | 399 | // DownloadOptions 400 | downloadOptionsCustomFilenameInput.value = info.title || "" 401 | if((info.title || "").includes("-")){ 402 | downloadOptionsSongModeArtistInput.value = info.title.split("-")[0].trim() 403 | downloadOptionsSongModeTitleInput.value = info.title.split("-")[1].trim() 404 | }else{ 405 | downloadOptionsSongModeArtistInput.value = isYoutube() 406 | ? info.channel 407 | : info.uploader || "Unknown" 408 | downloadOptionsSongModeTitleInput.value = info.title || "output" 409 | } 410 | 411 | downloadOptionsApplyThumbnailPreview.src = selectedThumbnail 412 | 413 | videoInfoData = info 414 | 415 | if(["preset-max", "preset-max-video", "preset-max-audio"].includes(localStorage.getItem("downloadFormat"))){ 416 | videoFormatSelector.value = localStorage.getItem("downloadFormat") 417 | videoFormatSelector.dispatchEvent(new Event("input")) 418 | } 419 | }) 420 | videoPreviewContinue.disabled = true 421 | videoFormatSelector.addEventListener("input", _ => { 422 | function getFormatObjFromId(formatId){ 423 | for(let format of videoInfoData.formats){ 424 | if(format.format_id === formatId.toString()){ 425 | return format 426 | } 427 | } 428 | return null 429 | } 430 | 431 | if(videoFormatSelector.value.includes("preset-max") || isPlaylist()){ 432 | videoFormatCustomCollapse.hide() 433 | if(isPlaylist()){ 434 | switch (videoFormatSelector.value.split("preset-")[1]){ 435 | case "max-video": 436 | selectedDownloadFormat = "bestvideo" 437 | selectedDownloadOutputMode = "video" 438 | break 439 | case "max-audio": 440 | selectedDownloadFormat = "bestaudio" 441 | selectedDownloadOutputMode = "audio" 442 | break 443 | default: 444 | selectedDownloadFormat = "bestaudio+bestvideo" 445 | selectedDownloadOutputMode = "normal" 446 | break 447 | } 448 | videoFormatInfoBox.textContent = "" 449 | }else{ 450 | let formatTarget = videoInfoData.format_id.split("+") 451 | let videoFormat = formatTarget[0] 452 | let audioFormat = formatTarget.pop() 453 | 454 | let selectedFormatObj 455 | switch (videoFormatSelector.value.split("preset-")[1]){ 456 | case "max-video": 457 | selectedDownloadFormat = videoFormat 458 | selectedDownloadOutputMode = "video" 459 | 460 | selectedFormatObj = getFormatObjFromId(videoFormat) 461 | videoFormatInfoBox.innerText = `Video: ${formatVideoFormat(selectedFormatObj)}\nAudio: None` 462 | break 463 | case "max-audio": 464 | selectedDownloadFormat = audioFormat 465 | selectedDownloadOutputMode = "audio" 466 | 467 | selectedFormatObj = getFormatObjFromId(audioFormat) 468 | videoFormatInfoBox.innerText = `Video: None\nAudio: ${formatAudioFormat(selectedFormatObj)}` 469 | break 470 | default: 471 | selectedDownloadFormat = videoInfoData.format_id 472 | selectedDownloadOutputMode = "normal" 473 | 474 | let selectedVideoFormatObj = getFormatObjFromId(videoFormat) 475 | let selectedAudioFormatObj = getFormatObjFromId(audioFormat) 476 | videoFormatInfoBox.innerText = `Video: ${formatVideoFormat(selectedVideoFormatObj)}\nAudio: ${formatAudioFormat(selectedAudioFormatObj)}` 477 | break 478 | } 479 | selectedDownloadFormatContainer = videoInfoData.ext 480 | } 481 | 482 | videoPreviewContinue.disabled = false 483 | callAttention(videoPreviewContinue) 484 | }else if(videoFormatSelector.value === "custom"){ 485 | videoFormatCustomCollapse.show() 486 | callAttention(videoFormatCustomBox) 487 | updateCustomFormatSelection() 488 | videoFormatInfoBox.textContent = "" 489 | }else{ 490 | videoFormatCustomCollapse.hide() 491 | selectedDownloadFormat = videoFormatSelector.value 492 | selectedDownloadFormatContainer = getFormatObjFromId(videoFormatSelector.value)?.ext 493 | selectedDownloadOutputMode = "custom" 494 | videoFormatInfoBox.textContent = "" 495 | videoPreviewContinue.disabled = false 496 | callAttention(videoPreviewContinue) 497 | } 498 | }) 499 | 500 | function updateCustomFormatSelection(){ 501 | let video = videoFormatCustomVideoSelector.value 502 | let audio = videoFormatCustomAudioSelector.value 503 | if(video === "" || audio === ""){ 504 | videoPreviewContinue.disabled = true 505 | if(video === "" && audio === "") callAttention(videoFormatCustomBox) 506 | else if(video === "") callAttention(videoFormatCustomVideoSelector) 507 | else if(audio === "") callAttention(videoFormatCustomAudioSelector) 508 | return 509 | } 510 | videoPreviewContinue.disabled = false 511 | if(video !== "none" && audio !== "none"){ 512 | selectedDownloadFormat = `${video}+${audio}` 513 | selectedDownloadFormatContainer = videoInfoData.formats.find(format => format.format_id === video)?.ext 514 | selectedDownloadOutputMode = "video" 515 | }else if(video !== "none" && audio === "none"){ 516 | selectedDownloadFormat = video 517 | selectedDownloadFormatContainer = videoInfoData.formats.find(format => format.format_id === video)?.ext 518 | selectedDownloadOutputMode = "video" 519 | }else if(video === "none" && audio !== "none"){ 520 | selectedDownloadFormatContainer = audio 521 | selectedDownloadFormatContainer = videoInfoData.formats.find(format => format.format_id === audio)?.ext 522 | selectedDownloadOutputMode = "audio" 523 | }else{ 524 | videoPreviewContinue.disabled = true 525 | callAttention(videoFormatCustomBox) 526 | } 527 | videoFormatInfoBox.textContent = "" 528 | } 529 | videoFormatCustomVideoSelector.addEventListener("input", updateCustomFormatSelection) 530 | videoFormatCustomAudioSelector.addEventListener("input", updateCustomFormatSelection) 531 | 532 | videoPreviewContinue.addEventListener("click", _ => { 533 | if(videoPreviewContinue.disabled) return 534 | 535 | function resetSelection(){ 536 | videoFileTypeSelector.querySelector("option[disabled]").selected = true 537 | outputLocationInput.parentElement.querySelector("button").disabled = true 538 | selectedDownloadOutputFileType = null 539 | downloadOptionsCollapse.hide() 540 | } 541 | function getSelected(){ 542 | for (let elem of videoFileTypeSelector.querySelectorAll("option")){ 543 | if(elem.getAttribute("value") === videoFileTypeSelector.value){ 544 | return elem 545 | } 546 | } 547 | return videoFileTypeSelector.querySelector("option[disabled]") 548 | } 549 | switch (selectedDownloadOutputMode){ 550 | case "video": 551 | case "normal": 552 | videoFileTypeSelector.querySelector("optgroup[label=Video]").disabled = false 553 | videoFileTypeSelector.querySelector("optgroup[label=Audio]").disabled = true 554 | if(getSelected().parentElement.getAttribute("label") !== "Video"){ 555 | resetSelection() 556 | } 557 | break 558 | case "audio": 559 | videoFileTypeSelector.querySelector("optgroup[label=Video]").disabled = true 560 | videoFileTypeSelector.querySelector("optgroup[label=Audio]").disabled = false 561 | if(getSelected().parentElement.getAttribute("label") !== "Audio"){ 562 | resetSelection() 563 | } 564 | break 565 | default: 566 | videoFileTypeSelector.querySelector("optgroup[label=Video]").disabled = false 567 | videoFileTypeSelector.querySelector("optgroup[label=Audio]").disabled = false 568 | break 569 | } 570 | 571 | if(localStorage.getItem("exportFormat") && (!["", "none"].includes(localStorage.getItem("exportFormat")))){ 572 | videoFileTypeSelector.value = localStorage.getItem("exportFormat") 573 | videoFileTypeSelector.dispatchEvent(new Event("input")) 574 | } 575 | 576 | if(videoFileTypeSelector.value === "Please select an output filetype"){ 577 | callAttention(videoFileTypeSelector) 578 | }else if(outputLocationInput.value !== ""){ 579 | callAttention(downloadOptionsStart) 580 | } 581 | 582 | videoInfoCollapse.hide() 583 | videoDownloadCollapse.show() 584 | }) 585 | downloadOptionsBack.addEventListener("click", _ => { 586 | videoDownloadCollapse.hide() 587 | videoInfoCollapse.show() 588 | }) 589 | videoFileTypeSelector.addEventListener("input", _ => { 590 | outputLocationInput.parentElement.querySelector("button").disabled = false 591 | if(localStorage.getItem("defaultFolder") && localStorage.getItem("defaultFolder") !== ""){ 592 | selectedDownloadOutputFilePath = localStorage.getItem("defaultFolder") 593 | }else{ 594 | callAttention(outputLocationInput.parentElement.querySelector("button")) 595 | } 596 | 597 | downloadOptionsSongModeCheckbox.checked = localStorage.getItem("songMode") === "true" 598 | downloadOptionsSongModeCheckbox.dispatchEvent(new Event("change")) 599 | downloadOptionsApplyThumbnail.checked = localStorage.getItem("thumbnail") === "true" 600 | downloadOptionsApplyThumbnail.dispatchEvent(new Event("change")) 601 | 602 | selectedDownloadOutputFileType = videoFileTypeSelector.value 603 | updateDownloadOutputLocation() 604 | }) 605 | outputLocationInput.parentElement.querySelector("button").addEventListener("click", _ => { 606 | if(outputLocationInput.parentElement.querySelector("button").disabled) return 607 | window.dialog.showDialog({ 608 | title: "Select an output folder", 609 | buttonLabel: "Choose Folder", 610 | properties: ["openDirectory"] 611 | }, "selectLocation") 612 | }) 613 | function updateDownloadOutputLocation(){ 614 | if(!selectedDownloadOutputFilePath) return 615 | 616 | downloadOptionsCollapse.show() 617 | let addition = videoInfoData?.title || "unknown" 618 | if(downloadOptionsCustomFilenameCheckbox.checked){ 619 | downloadOptionsSongModeAffectsFileName.disabled = true 620 | downloadOptionsSongModeAffectsFileName.checked = false 621 | addition = downloadOptionsCustomFilenameInput.value.trim() 622 | }else if( 623 | downloadOptionsSongModeCheckbox.checked 624 | && (downloadOptionsSongModeAffectsFileName.checked || downloadOptionsSongModeAffectsFileName.disabled) 625 | && !isPlaylist() 626 | ){ 627 | downloadOptionsSongModeAffectsFileName.disabled = false 628 | downloadOptionsSongModeAffectsFileName.checked = true 629 | addition = `${downloadOptionsSongModeArtistInput.value.trim()} - ${downloadOptionsSongModeTitleInput.value.trim()}` 630 | } 631 | 632 | if(!fileNameFormat.test(addition)){ 633 | outputLocationInput.classList.add("text-danger") 634 | downloadOptionsStart.disabled = true 635 | callAttention(outputLocationInput) 636 | }else{ 637 | outputLocationInput.classList.remove("text-danger") 638 | downloadOptionsStart.disabled = false 639 | } 640 | 641 | addition += isPlaylist() ? "/" : `.${selectedDownloadOutputFileType}` 642 | 643 | selectedDownloadOutputFile = window.utils.path.join(selectedDownloadOutputFilePath, addition) 644 | outputLocationInput.value = selectedDownloadOutputFile 645 | } 646 | window.dialog.on("showDialogResponse:selectLocation", (_, response) => { 647 | if(response.canceled) return 648 | selectedDownloadOutputFilePath = response.filePaths[0] 649 | 650 | updateDownloadOutputLocation() 651 | 652 | callAttention(downloadOptionsStart) 653 | downloadOptionsCollapse.show() 654 | }) 655 | function setDownloadOptionsCustomFilenameCollapseState(){ 656 | if(downloadOptionsCustomFilenameCheckbox.checked === downloadOptionsCustomFilenameCollapse._isShown()) return 657 | if(downloadOptionsCustomFilenameCheckbox.checked){ 658 | downloadOptionsCustomFilenameCollapse.show() 659 | }else{ 660 | downloadOptionsCustomFilenameCollapse.hide() 661 | } 662 | updateDownloadOutputLocation() 663 | } 664 | downloadOptionsCustomFilenameCheckbox.addEventListener("input", setDownloadOptionsCustomFilenameCollapseState) 665 | setInterval(setDownloadOptionsCustomFilenameCollapseState, 250) 666 | function setDownloadOptionsSongModeCollapseState(){ 667 | if(isPlaylist()){ 668 | if(downloadOptionsSongModeCollapse._isShown()) downloadOptionsSongModeCollapse.hide() 669 | return 670 | } 671 | if(downloadOptionsSongModeCheckbox.checked === downloadOptionsSongModeCollapse._isShown()) return 672 | if(downloadOptionsSongModeCheckbox.checked){ 673 | downloadOptionsSongModeCollapse.show() 674 | }else{ 675 | downloadOptionsSongModeCollapse.hide() 676 | } 677 | updateDownloadOutputLocation() 678 | } 679 | downloadOptionsSongModeCheckbox.addEventListener("input", setDownloadOptionsSongModeCollapseState) 680 | setInterval(setDownloadOptionsSongModeCollapseState, 250) 681 | 682 | downloadOptionsCustomFilenameInput.addEventListener("input", updateDownloadOutputLocation) 683 | downloadOptionsSongModeArtistInput.addEventListener("input", updateDownloadOutputLocation) 684 | downloadOptionsSongModeTitleInput.addEventListener("input", updateDownloadOutputLocation) 685 | downloadOptionsSongModeAffectsFileName.addEventListener("input", updateDownloadOutputLocation) 686 | 687 | downloadOptionsApplyThumbnail.addEventListener("input", setDownloadOptionsApplyThumbnailCollapseState) 688 | setInterval(setDownloadOptionsApplyThumbnailCollapseState, 250) 689 | function setDownloadOptionsApplyThumbnailCollapseState(){ 690 | if(isPlaylist()){ 691 | if(downloadOptionsApplyThumbnailCollapse._isShown()) downloadOptionsApplyThumbnailCollapse.hide() 692 | return 693 | } 694 | if(downloadOptionsApplyThumbnail.checked === downloadOptionsApplyThumbnailCollapse._isShown()) return 695 | if(downloadOptionsApplyThumbnail.checked){ 696 | downloadOptionsApplyThumbnailCollapse.show() 697 | }else{ 698 | downloadOptionsApplyThumbnailCollapse.hide() 699 | } 700 | } 701 | function updateApplyThumbnailPreview(){ 702 | if(downloadOptionsApplyThumbnailCustomCheckbox.checked){ 703 | downloadOptionsApplyThumbnailPreview.src = selectedThumbnail 704 | }else{ 705 | selectedThumbnail = videoInfoData?.thumbnail 706 | downloadOptionsApplyThumbnailPreview.src = selectedThumbnail 707 | } 708 | } 709 | downloadOptionsApplyThumbnailCustomCheckbox.addEventListener("input", _ => { 710 | downloadOptionsApplyThumbnailCustomInput.disabled = !downloadOptionsApplyThumbnailCustomCheckbox.checked 711 | updateApplyThumbnailPreview() 712 | }) 713 | downloadOptionsApplyThumbnailCustomInput.addEventListener("click", _ => { 714 | window.dialog.showDialog({ 715 | title: "Select a custom thumbnail", 716 | buttonLabel: "Choose File", 717 | properties: ["openFile"], 718 | filters: [ 719 | {name: 'Image (png, jpg, jpeg)', extensions: ['png', "jpg", "jpeg"]} 720 | ] 721 | }, "chooseThumbnail") 722 | }) 723 | window.dialog.on("showDialogResponse:chooseThumbnail", (_, resp) => { 724 | if(resp.canceled) return 725 | selectedThumbnail = resp.filePaths[0] 726 | updateApplyThumbnailPreview() 727 | }) 728 | 729 | downloadOptionsStart.addEventListener("click", _ => { 730 | downloadOptionsCollapse.hide() 731 | downloadProgressCollapse.show() 732 | 733 | let artistData = downloadOptionsSongModeArtistInput.value.split(",").map(v => v.trim()).join("/") 734 | let metadata = downloadOptionsSongModeCheckbox.checked 735 | ? { 736 | title: downloadOptionsSongModeTitleInput.value, 737 | artist: artistData, 738 | author: artistData 739 | } 740 | : null 741 | 742 | let thumbnail = downloadOptionsApplyThumbnail.checked 743 | ? { 744 | external: !downloadOptionsApplyThumbnailCustomCheckbox.checked, 745 | link: selectedThumbnail 746 | } 747 | : null 748 | 749 | window.downloader.startDownload( 750 | isPlaylist(), 751 | urlInput.value, 752 | selectedDownloadFormat, 753 | selectedDownloadFormatContainer, 754 | selectedDownloadOutputFile, 755 | selectedDownloadOutputFileType, 756 | metadata, 757 | thumbnail 758 | ) 759 | }) 760 | window.downloader.on("progress:info", (_, info) => { 761 | downloadProgressBar.textContent = info 762 | 763 | videoFileTypeSelector.disabled = true 764 | outputLocationInput.parentElement.querySelector("button").disabled = true 765 | downloadProgressAbortButton.disabled = false 766 | 767 | downloadProgressInfo.textContent = "" 768 | }) 769 | window.downloader.on("progress:mode", (_, mode) => { 770 | switch (mode){ 771 | case "unstable": 772 | downloadProgressBar.style.width = "100%" 773 | downloadProgressBar.classList.add("progress-bar-striped", "progress-bar-animated", "bg-info") 774 | window.controls.setProgressBar(2) 775 | break 776 | case "stable": 777 | default: 778 | downloadProgressBar.style.width = "0" 779 | downloadProgressBar.classList.remove("progress-bar-striped", "progress-bar-animated", "bg-info") 780 | } 781 | }) 782 | window.downloader.on("progress:data", (_, data) => { 783 | downloadProgressBar.textContent = `${data.progress}%` 784 | downloadProgressBar.style.width = `${data.progress}%` 785 | downloadProgressInfo.innerText = 786 | ` 787 | Downloaded ${Math.round(data.transferred * 100)/100}MiB of ${data.size} 788 | Downloading with ${data.speed} 789 | Estimated time left: ${data.estimated} 790 | ` 791 | window.controls.setProgressBar(data.progress/100) 792 | }) 793 | window.downloader.on("progress:downloadComplete", _ => { 794 | window.controls.setProgressBar(0) 795 | downloadProgressAbortButton.disabled = true 796 | downloadProgressInfo.textContent = "Download finished" 797 | }) 798 | downloadProgressAbortButton.addEventListener("click", _ => { 799 | if(downloadProgressAbortButton.classList.contains("btn-info")){ 800 | downloadProgressAbortButton.classList.replace("btn-info", "btn-warning") 801 | downloadProgressAbortButton.textContent = "Abort Download" 802 | downloadProgressCollapse.hide() 803 | downloadOptionsCollapse.show() 804 | }else { 805 | window.downloader.killDownload() 806 | } 807 | }) 808 | window.downloader.on("progress:downloadAborted", _ => { 809 | window.controls.setProgressBar(0) 810 | downloadProgressAbortButton.classList.replace("btn-warning", "btn-info") 811 | downloadProgressAbortButton.textContent = "Go Back" 812 | downloadProgressInfo.textContent = "Download aborted" 813 | }) 814 | window.downloader.on("progress:log", (_, data) => { 815 | let logElem = document.createElement("li") 816 | logElem.textContent = data.message 817 | logElem.classList.add(data.tag) 818 | downloadProgressLog.append(logElem) 819 | downloadProgressLog.scrollTo(0, downloadProgressLog.scrollHeight) 820 | }) 821 | 822 | 823 | 824 | // Temporary dev stuff 825 | setTimeout(_ => { 826 | // urlInput.value = "https://www.youtube.com/watch?v=WZRh2yUDUvQ&list=PLC1og_v3eb4gIg85NKJPj6jewk5YvWAtx&pp=iAQB" 827 | // urlInputApply.click() 828 | 829 | // settingsCollapse.hide() 830 | // selectedDownloadFormat = "123" 831 | // selectedDownloadOutputMode = "custom" 832 | // videoDownloadCollapse.show() 833 | // downloadProgressCollapse.show() 834 | }, 500) 835 | 836 | 837 | window.downloader.on("error", (_, err) => { 838 | errorOut("Error", err.stderr) 839 | }) 840 | -------------------------------------------------------------------------------- /static/bootstrap/bootstrap.bundle.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); 7 | //# sourceMappingURL=bootstrap.bundle.min.js.map --------------------------------------------------------------------------------