├── .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 |
YT Downloader
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 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | > The Recode: Now with a new awesome looking UI design!
12 | >
13 | > Video info preview and easy/automatic format selection
14 | > 
15 | >
16 | > Optimized for audio downloads with ability to add custom metadata and a thumbnail
17 | > 
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 |
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 |
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 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
181 |
182 |
183 |
184 |
185 |
186 |
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 |
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
--------------------------------------------------------------------------------