├── js ├── context_menu.js ├── extensions │ └── lyrics │ │ ├── plugin.json │ │ └── index.js ├── imports.js ├── backtrace.js ├── plugins.js ├── language_pack.js ├── downloader.js ├── json_database.js ├── keyboard_shortcuts.js ├── image_helper.js ├── database.js ├── pip.js └── render.js ├── preload.js ├── windows-setup ├── .gitignore ├── build_setup.cmd └── setup.py ├── assets ├── banner.png ├── newflow.ico ├── newflow.png ├── screenshots │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ └── GALLERY.md ├── inner │ ├── playlist_liked.png │ └── playlist_watch_later.png ├── material-symbols │ ├── pause.png │ ├── play_arrow.png │ ├── skip_next.png │ ├── skip_previous.png │ ├── keyboard_arrow_up.png │ ├── keyboard_arrow_down.png │ ├── keyboard_arrow_left.png │ └── keyboard_arrow_right.png ├── fluent-icons │ ├── line_horizontal_1_16_regular.svg │ ├── maximize_16_regular.svg │ ├── dismiss_16_regular.svg │ └── square_multiple_16_regular.svg ├── bootstrap-icons │ ├── twitter-x.svg │ ├── telegram.svg │ └── whatsapp.svg ├── breeze-icons │ ├── go-up.svg │ ├── go-down.svg │ ├── window-restore.svg │ └── window-close.svg ├── yaru-icons │ ├── window-minimize-symbolic.svg │ ├── window-maximize-symbolic.svg │ ├── window-close-symbolic.svg │ └── window-restore-symbolic.svg └── newflow.svg ├── .gitignore ├── NewFlow.desktop ├── fonts └── material-symbols-rounded.woff2 ├── scripts ├── uninstall-desktop-file ├── remove_launcher.vbs ├── install-desktop-file ├── create_launcher_with_global_electron.vbs └── create_launcher.vbs ├── linux-setup ├── package.json ├── css ├── material-symbols.css ├── background-material.css ├── pip.css ├── layout.css └── styles.css ├── pip.html ├── index.js ├── languages.json ├── README.md ├── LICENSE ├── index.html └── LICENSE_CC_BY_NC_ND_4.0 /js/context_menu.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | window.electron = require('@electron/remote'); -------------------------------------------------------------------------------- /windows-setup/.gitignore: -------------------------------------------------------------------------------- 1 | *.spec 2 | dist 3 | build 4 | *.exe 5 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/banner.png -------------------------------------------------------------------------------- /assets/newflow.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/newflow.ico -------------------------------------------------------------------------------- /assets/newflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/newflow.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | electron-data 2 | yt-extractor 3 | dbs/*.json 4 | js/__.js 5 | window_config.json 6 | \!TODO -------------------------------------------------------------------------------- /assets/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/1.png -------------------------------------------------------------------------------- /assets/screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/10.png -------------------------------------------------------------------------------- /assets/screenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/11.png -------------------------------------------------------------------------------- /assets/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/2.png -------------------------------------------------------------------------------- /assets/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/3.png -------------------------------------------------------------------------------- /assets/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/4.png -------------------------------------------------------------------------------- /assets/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/5.png -------------------------------------------------------------------------------- /assets/screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/6.png -------------------------------------------------------------------------------- /assets/screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/7.png -------------------------------------------------------------------------------- /assets/screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/8.png -------------------------------------------------------------------------------- /assets/screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/screenshots/9.png -------------------------------------------------------------------------------- /assets/inner/playlist_liked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/inner/playlist_liked.png -------------------------------------------------------------------------------- /assets/material-symbols/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/pause.png -------------------------------------------------------------------------------- /NewFlow.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=NewFlow 3 | Comment=Your free video player client for YouTube™ 4 | Type=Application 5 | -------------------------------------------------------------------------------- /assets/inner/playlist_watch_later.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/inner/playlist_watch_later.png -------------------------------------------------------------------------------- /assets/material-symbols/play_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/play_arrow.png -------------------------------------------------------------------------------- /assets/material-symbols/skip_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/skip_next.png -------------------------------------------------------------------------------- /fonts/material-symbols-rounded.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/fonts/material-symbols-rounded.woff2 -------------------------------------------------------------------------------- /assets/material-symbols/skip_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/skip_previous.png -------------------------------------------------------------------------------- /assets/material-symbols/keyboard_arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/keyboard_arrow_up.png -------------------------------------------------------------------------------- /assets/material-symbols/keyboard_arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/keyboard_arrow_down.png -------------------------------------------------------------------------------- /assets/material-symbols/keyboard_arrow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/keyboard_arrow_left.png -------------------------------------------------------------------------------- /assets/material-symbols/keyboard_arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malisipi/NewFlow/HEAD/assets/material-symbols/keyboard_arrow_right.png -------------------------------------------------------------------------------- /windows-setup/build_setup.cmd: -------------------------------------------------------------------------------- 1 | :: pip install PyInstaller 2 | python -m PyInstaller setup.py --noconsole --windowed --onefile -i ../assets/newflow.ico -------------------------------------------------------------------------------- /scripts/uninstall-desktop-file: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm ~/.local/share/icons/hicolor/scalable/apps/newflow.svg 3 | rm ~/.local/share/applications/NewFlow.desktop -------------------------------------------------------------------------------- /js/extensions/lyrics/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lyrics for NewFlow", 3 | "description": "Find lyrics of your music videos", 4 | "developer": "malisipi", 5 | "version": "1.0.0" 6 | } -------------------------------------------------------------------------------- /assets/fluent-icons/line_horizontal_1_16_regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /linux-setup: -------------------------------------------------------------------------------- 1 | mkdir "$HOME/opt/" 2 | cd "$HOME/opt/" 3 | git clone https://github.com/malisipi/NewFlow 4 | cd NewFlow 5 | git clone https://github.com/malisipi/yt-extractor.js 6 | mv "yt-extractor.js" "yt-extractor" 7 | ./scripts/install-desktop-file -------------------------------------------------------------------------------- /assets/fluent-icons/maximize_16_regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/bootstrap-icons/twitter-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/fluent-icons/dismiss_16_regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NewFlow", 3 | "version": "0.0.1", 4 | "description": "A YouTube desktop client", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NEWFLOW_DEBUG=1 electron . --user-data-dir=./electron-data --enable-features=UseOzonePlatform --ozone-platform=wayland" 8 | }, 9 | "author": "malisipi", 10 | "license": "Apache-2.0" 11 | } 12 | -------------------------------------------------------------------------------- /scripts/remove_launcher.vbs: -------------------------------------------------------------------------------- 1 | Set fso = CreateObject("Scripting.FileSystemObject") 2 | Set shell = CreateObject("WScript.Shell") 3 | 4 | desktop_dir = shell.SpecialFolders("Desktop") 5 | apps_dir = shell.ExpandEnvironmentStrings("%APPDATA%") & "\\Microsoft\\Windows\\Start Menu\\Programs\\" 6 | 7 | fso.DeleteFile(desktop_dir & "\\NewFlow.lnk") 8 | fso.DeleteFile(apps_dir & "\\NewFlow.lnk") -------------------------------------------------------------------------------- /assets/breeze-icons/go-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/breeze-icons/go-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/fluent-icons/square_multiple_16_regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/breeze-icons/window-restore.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/screenshots/GALLERY.md: -------------------------------------------------------------------------------- 1 | # NewFlow Gallery 2 | 3 | ## Main Page 4 | ![](./1.png) 5 | 6 | ## Search 7 | ![](./2.png) 8 | ![](./3.png) 9 | 10 | ## Player 11 | ![](./4.png) 12 | 13 | ## Side Navigation 14 | 15 | ![](./5.png) 16 | 17 | ## Queue view 18 | 19 | ![](./6.png) 20 | 21 | ## Comments 22 | 23 | ![](./7.png) 24 | 25 | ## Following Page 26 | 27 | ![](./8.png) 28 | 29 | ## Feeds 30 | 31 | ![](./9.png) 32 | 33 | ## Pop-Up Video Player 34 | 35 | ![](./10.png) 36 | 37 | ## Artist View 38 | 39 | ![](./11.png) -------------------------------------------------------------------------------- /js/imports.js: -------------------------------------------------------------------------------- 1 | var yt_extractor = require("./yt-extractor"); 2 | var fs = require("node:fs/promises"); 3 | var path = require('node:path'); 4 | window.addEventListener("DocumentReady", () => { 5 | window.http = require("node:http"); 6 | window.https = require("node:https"); 7 | window.fs_legacy = require("node:fs"); 8 | window.child_process = require("node:child_process"); 9 | }) 10 | 11 | fs.exists = async (file_name) => { 12 | try { 13 | await fs.realpath(file_name); 14 | return true; 15 | } catch { 16 | return false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /js/backtrace.js: -------------------------------------------------------------------------------- 1 | window.onerror = (message, source, lineno, colno, err) => { 2 | alert(`@${lineno}/${colno}: ${message} 3 | Source: ${source}, 4 | Full Error: ${err}`); 5 | console.warn(`@${lineno}/${colno}: ${message} 6 | Source: ${source}, 7 | Full Error: ${err}`); 8 | return true; 9 | }; 10 | 11 | window.addEventListener("DOMContentLoaded", () => { 12 | components.tabs.watch.video.$video.addEventListener("error", async event => { 13 | alert(`${event.target.error.code} / ${event.target.error.message}`); 14 | console.error(event.target.error); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /assets/yaru-icons/window-minimize-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/yaru-icons/window-maximize-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/install-desktop-file: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p ~/.local/share/icons/hicolor/scalable/apps/ 3 | cp ./assets/newflow.svg ~/.local/share/icons/hicolor/scalable/apps/newflow.svg 4 | cp NewFlow.desktop /tmp/_newflow 5 | echo Icon=$HOME/.local/share/icons/hicolor/scalable/apps/newflow.svg >> /tmp/_newflow 6 | echo "Categories=AudioVideo;Audio;Video;Player;TV;Network;" >> /tmp/_newflow 7 | echo Path=$(pwd) >> /tmp/_newflow 8 | echo Exec=electron $(pwd) $NEWFLOW_FLAGS >> /tmp/_newflow 9 | mkdir ~/.local/share/applications/ 10 | cp /tmp/_newflow ~/.local/share/applications/NewFlow.desktop 11 | rm /tmp/_newflow 12 | -------------------------------------------------------------------------------- /assets/breeze-icons/window-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /assets/bootstrap-icons/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /assets/yaru-icons/window-close-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /js/plugins.js: -------------------------------------------------------------------------------- 1 | var plugins = { 2 | list: () => {}, 3 | load: (plugin_id) => { 4 | console.info(`Plugin: ${plugin_id} is loaded`); 5 | let plugin_script = document.createElement("script"); 6 | document.head.append(plugin_script); 7 | plugin_script.setAttribute("plugin", plugin_id); 8 | plugin_script.addEventListener("load", (event) => { 9 | window[plugin_id].__load__(); 10 | }); 11 | plugin_script.src = `./js/extensions/${plugin_id}/index.js`; 12 | }, 13 | unload: (plugin_id) => { 14 | console.info(`Plugin: ${plugin_id} is unloaded`); 15 | Array.from(document.head.querySelectorAll("script[plugin]")).filter(a=>a.getAttribute("plugin") == plugin_id)?.[0]?.remove(); 16 | window[plugin_id].__unload__(); 17 | } 18 | }; -------------------------------------------------------------------------------- /css/material-symbols.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Symbols Rounded'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(../fonts/material-symbols-rounded.woff2); 6 | } 7 | 8 | material-symbol, .material-symbol { 9 | --size: 24px; 10 | font-family: 'Material Symbols Rounded'; 11 | font-weight: normal; 12 | font-style: normal; 13 | font-size: var(--size); 14 | line-height: 1; 15 | letter-spacing: normal; 16 | text-transform: none; 17 | display: inline-block; 18 | white-space: nowrap; 19 | word-wrap: normal; 20 | direction: ltr; 21 | -webkit-font-feature-settings: 'liga'; 22 | font-feature-settings: "liga"; 23 | -webkit-font-smoothing: antialiased; 24 | } 25 | 26 | material-symbol { 27 | height: var(--size); 28 | width: var(--size); 29 | } 30 | -------------------------------------------------------------------------------- /js/language_pack.js: -------------------------------------------------------------------------------- 1 | var i18n = { 2 | language_package: {}, 3 | get: async () => { 4 | let languages = await fetch("./languages.json"); 5 | languages = await languages.json(); 6 | let active_language = navigator.language; 7 | i18n.language_package = Object.keys(languages).map( 8 | key => ({ 9 | key:key, value:languages[key][active_language] ?? languages[key]["en"] ?? key 10 | }) 11 | ).reduce((pack, word) => { 12 | pack[word.key] = word.value; 13 | return pack; 14 | }, {}); 15 | }, 16 | text: (key) => { 17 | return i18n.language_package[key] ?? key; 18 | }, 19 | ready: null 20 | }; 21 | 22 | i18n.ready = window.i18n.get(); 23 | 24 | window.addEventListener("DOMContentLoaded", async () => { 25 | await i18n.ready; 26 | document.querySelectorAll("language-pack").forEach(text => text.replaceWith(i18n.text(text.getAttribute("key")))); 27 | }) -------------------------------------------------------------------------------- /js/downloader.js: -------------------------------------------------------------------------------- 1 | var downloader = { 2 | download: (uri, href) => { 3 | let $on = document.createElement("div"); 4 | $on.contentSize = 0; 5 | $on.uri = uri; 6 | $on.href = href; 7 | $on.state = "downloading"; 8 | let file = fs_legacy.createWriteStream(href); 9 | http_ = (new URL(uri).protocol == "http:") ? http : https; 10 | let request = http_.get(uri, response => { 11 | response.pipe(file); 12 | response.on("data", (e) => { 13 | $on.contentLength = Number(response.rawHeaders?.[(response.rawHeaders?.indexOf("Content-Length") ?? -1) + 1] ?? -1); 14 | $on.contentSize += e.length; 15 | $on.dispatchEvent(new CustomEvent("state-update")); 16 | }); 17 | 18 | file.on("finish", () => { 19 | file.close(); 20 | $on.state = "finish"; 21 | $on.dispatchEvent(new CustomEvent("downloaded")); 22 | }); 23 | }); 24 | return $on; 25 | } 26 | }; -------------------------------------------------------------------------------- /scripts/create_launcher_with_global_electron.vbs: -------------------------------------------------------------------------------- 1 | Set fso = CreateObject("Scripting.FileSystemObject") 2 | Set shell = CreateObject("WScript.Shell") 3 | script_dir = fso.GetParentFolderName(WScript.ScriptFullName) 4 | app_dir = fso.GetParentFolderName(script_dir) 5 | desktop_dir = shell.SpecialFolders("Desktop") 6 | apps_dir = shell.ExpandEnvironmentStrings("%APPDATA%") & "\\Microsoft\\Windows\\Start Menu\\Programs\\" 7 | 8 | Set desktop_link = shell.CreateShortcut(desktop_dir & "\NewFlow.lnk") 9 | desktop_link.Arguments = app_dir 10 | desktop_link.Description = "Your Free Client" 11 | desktop_link.IconLocation = app_dir & "\\assets\\newflow.ico" 12 | desktop_link.TargetPath = "electron.exe" 13 | desktop_link.WorkingDirectory = app_dir 14 | desktop_link.Save 15 | 16 | Set app_link = shell.CreateShortcut(apps_dir & "\NewFlow.lnk") 17 | app_link.Arguments = app_dir 18 | app_link.Description = "Your Free Client" 19 | app_link.IconLocation = app_dir & "\\assets\\newflow.ico" 20 | app_link.TargetPath = "electron.exe" 21 | app_link.WorkingDirectory = app_dir 22 | app_link.Save -------------------------------------------------------------------------------- /assets/yaru-icons/window-restore-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /scripts/create_launcher.vbs: -------------------------------------------------------------------------------- 1 | Set fso = CreateObject("Scripting.FileSystemObject") 2 | Set shell = CreateObject("WScript.Shell") 3 | script_dir = fso.GetParentFolderName(WScript.ScriptFullName) 4 | app_dir = fso.GetParentFolderName(script_dir) 5 | electron_dir = fso.GetParentFolderName(fso.GetParentFolderName(app_dir)) 6 | desktop_dir = shell.SpecialFolders("Desktop") 7 | apps_dir = shell.ExpandEnvironmentStrings("%APPDATA%") & "\\Microsoft\\Windows\\Start Menu\\Programs\\" 8 | 9 | Set desktop_link = shell.CreateShortcut(desktop_dir & "\NewFlow.lnk") 10 | desktop_link.Arguments = app_dir 11 | desktop_link.Description = "Your Free Client" 12 | desktop_link.IconLocation = app_dir & "\\assets\\newflow.ico" 13 | desktop_link.TargetPath = electron_dir & "\\electron.exe" 14 | desktop_link.WorkingDirectory = app_dir 15 | desktop_link.Save 16 | 17 | Set app_link = shell.CreateShortcut(apps_dir & "\NewFlow.lnk") 18 | app_link.Arguments = app_dir 19 | app_link.Description = "Your Free Client" 20 | app_link.IconLocation = app_dir & "\\assets\\newflow.ico" 21 | app_link.TargetPath = electron_dir & "\\electron.exe" 22 | app_link.WorkingDirectory = app_dir 23 | app_link.Save -------------------------------------------------------------------------------- /js/json_database.js: -------------------------------------------------------------------------------- 1 | class JsonDatabase { 2 | #config; 3 | /* 4 | * defaultStructure(null) 5 | * defaultStructureFrom(null) 6 | * fill_missing(false) 7 | */ 8 | file_name; 9 | content; 10 | 11 | async init (){ 12 | this.autosave = this.#config.disable_autosave != true; 13 | 14 | let content = this.#config.defaultStructure ?? "{}"; 15 | try { 16 | content = String(await fs.readFile(this.file_name)); 17 | } catch {}; 18 | if(typeof(content) == "object") { 19 | this.content = content; 20 | } else { 21 | this.content = JSON.parse(content); 22 | }; 23 | await this.#fill_missing(); 24 | } 25 | 26 | async #fill_missing (){ 27 | //this.#config.defaultStructure 28 | } 29 | 30 | async update_file (){ 31 | console.warn("Updated: " + this.file_name); 32 | await fs.writeFile(this.file_name, JSON.stringify(this.content)) 33 | } 34 | 35 | constructor(file_name, config = {}) { 36 | this.file_name = file_name; 37 | this.#config = config; 38 | return this; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /assets/bootstrap-icons/whatsapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /css/background-material.css: -------------------------------------------------------------------------------- 1 | body[material] { 2 | &, & video { 3 | background:transparent !important; 4 | } 5 | 6 | & .sidenav[open] { 7 | background: var(--background-color, black); 8 | border-radius: 5px; 9 | } 10 | } 11 | 12 | body[material="noise"] { 13 | & :fullscreen::after, 14 | &::after { 15 | content: ""; 16 | z-index: -2; 17 | opacity: 1; 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | border-radius: 5px; 24 | background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='6' numOctaves='1' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); 25 | } 26 | 27 | & :fullscreen::before, 28 | &::before { 29 | content: ""; 30 | z-index: -1; 31 | background: var(--background-color, black); 32 | opacity: 0.5; 33 | position: fixed; 34 | top: 0; 35 | left: 0; 36 | width: 100%; 37 | height: 100%; 38 | border-radius: 5px; 39 | } 40 | } -------------------------------------------------------------------------------- /pip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Picture-in-Picture 7 | 8 | 9 | 10 | 11 | 12 |
13 | close 14 |
0:00/0:00
15 | 16 |
17 | volume_up 18 | skip_previous 19 | fast_rewind 20 | play_arrow 21 | fast_forward 22 | skip_next 23 | fullscreen 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /js/keyboard_shortcuts.js: -------------------------------------------------------------------------------- 1 | document.body.addEventListener("keydown", event => { 2 | if(document.activeElement.tagName.toLowerCase() === "input") return; 3 | switch(components.tabs.$active()){ 4 | case "watch": { 5 | switch(event.code){ 6 | case "KeyK": 7 | case "Space": { 8 | components.tabs.watch.video.$_play_pause(); 9 | break; 10 | }; 11 | case "KeyJ": 12 | case "ArrowLeft": { 13 | components.tabs.watch.video.$_seekby(-10); 14 | break; 15 | }; 16 | case "KeyL": 17 | case "ArrowRight": { 18 | components.tabs.watch.video.$_seekby(10); 19 | break; 20 | }; 21 | case "KeyP": { 22 | components.tabs.watch.video.$_previous(); 23 | break; 24 | }; 25 | case "KeyN": { 26 | components.tabs.watch.video.$_next(); 27 | break; 28 | }; 29 | case "KeyT": { 30 | components.tabs.watch.video.$_theatre(); 31 | break; 32 | }; 33 | case "KeyF": { 34 | components.tabs.watch.video.$_fullscreen(); 35 | break; 36 | }; 37 | case "ArrowUp": { 38 | components.tabs.watch.video.$_volume(Math.min(components.tabs.watch.video.$volume()+0.05,1)); 39 | break; 40 | }; 41 | case "ArrowDown": { 42 | components.tabs.watch.video.$_volume(Math.max(components.tabs.watch.video.$volume()-0.05,0)); 43 | break; 44 | }; 45 | default: { 46 | return; 47 | } 48 | } 49 | break; 50 | } 51 | default: { 52 | return; 53 | } 54 | }; 55 | event.preventDefault(); 56 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | 3 | process.chdir(electron.app.getAppPath()); 4 | 5 | const remote = require('@electron/remote/main'); 6 | const path = require("node:path"); 7 | const fs = require("node:fs"); 8 | 9 | remote.initialize(); 10 | 11 | electron.app.on("ready", async _ => { 12 | let window_config = {}; 13 | try { 14 | window_config = JSON.parse(await fs.readFileSync("./window_config.json")); 15 | } catch {}; 16 | window_config.material = {"mica":"mica","acrylic":"acrylic","noise":"noise"}[window_config.material] ?? "default"; 17 | window_config.transparent = window_config.material != "default"; 18 | 19 | window_config.args = [`--newflow-material=${window_config.material}`]; 20 | 21 | if(!window_config.transparent) { 22 | window_config.titlebar_style = ["default", "hidden"][Number(window_config.system_titlebar)]; 23 | window_config.titlebar_overlay = [false, {color: "#00000000", symbolColor: "#FFFFFF", height: 30}][Number(window_config.system_titlebar)]; 24 | if(window_config.system_titlebar){ 25 | window_config.args = [...window_config.args, "--newflow-system-titlebar"]; 26 | }; 27 | } else { 28 | window_config.titlebar_style = "default"; 29 | window_config.titlebar_overlay = false; 30 | } 31 | 32 | newflow = new electron.BrowserWindow({ 33 | width: 800, 34 | height: 600, 35 | frame: false, 36 | transparent: window_config.transparent, 37 | titleBarStyle: window_config.titlebar_style, 38 | titleBarOverlay: window_config.titlebar_overlay, 39 | webPreferences: { 40 | nodeIntegration: true, 41 | contextIsolation: false, 42 | nodeIntegrationInWorker: true, 43 | backgroundThrottling: false, 44 | preload: path.join(__dirname, 'preload.js'), 45 | additionalArguments: window_config.args 46 | } 47 | }); 48 | newflow.loadFile("index.html"); 49 | remote.enable(newflow.webContents); 50 | 51 | newflow.on("closed", _ => { 52 | electron.app.quit(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /js/extensions/lyrics/index.js: -------------------------------------------------------------------------------- 1 | var lyrics = { 2 | components: { 3 | $: null, 4 | title: null, 5 | lyrics: null, 6 | get: null, 7 | info: null 8 | }, 9 | parser: new DOMParser(), 10 | get_lyrics: async (title) => { 11 | try { 12 | title = title.replace(/[\[\(\{].*[\]\)\}]/g,"").replace(/\|.*/g,"").trim(); 13 | let search_page = await fetch("https://genius.com/api/search/multi?per_page=5&q=" + encodeURIComponent(title)); 14 | let search_data = await search_page.json() 15 | let lyric_page = await fetch(search_data?.response?.sections?.[0]?.hits?.[0]?.result?.relationships_index_url) 16 | let lyric_page_data = await lyric_page.text() 17 | let lyric_page_document = lyrics.parser.parseFromString(lyric_page_data, "text/html"); 18 | return lyric_page_document.querySelector(`[data-lyrics-container="true"]`).innerHTML.replace(/\<[\\]*br\>/g,"\n").replace(/<[\\]*[^\>]+\>/g,""); 19 | } catch { 20 | return "Unable to extract lyrics"; 21 | } 22 | }, 23 | __load__: () => { 24 | lyrics.components.$ = document.createElement("div"); 25 | lyrics.components.$.style = "display: flex; flex-direction: column; gap: 5px;"; 26 | components.tabs.watch.panels.$.append(lyrics.components.$); 27 | lyrics.components.title = document.createElement("h1"); 28 | lyrics.components.title.innerText = "Lyrics"; 29 | lyrics.components.$.append(lyrics.components.title); 30 | lyrics.components.get = document.createElement("button"); 31 | lyrics.components.get.innerText = "Get Lyrics"; 32 | lyrics.components.get.addEventListener("click", async () => { 33 | let the_lyrics = await lyrics.get_lyrics(components.tabs.watch.$$response.title); 34 | lyrics.components.lyrics.innerText = the_lyrics; 35 | }); 36 | lyrics.components.get.style = "width: 100%; padding: 5px; background: var(--seconder-background-color); border: none; border-radius: var(--border-radius-size);"; 37 | lyrics.components.$.append(lyrics.components.get); 38 | lyrics.components.lyrics = document.createElement("div"); 39 | lyrics.components.lyrics.style = "user-select:text;"; 40 | lyrics.components.$.append(lyrics.components.lyrics); 41 | lyrics.components.info = document.createElement("div"); 42 | lyrics.components.info.innerText = "Lyric data is taken from Genius.\nThis plugin is not supported by Genius."; 43 | lyrics.components.$.append(lyrics.components.info); 44 | }, 45 | __unload__: () => { 46 | lyrics.components.$.remove(); 47 | lyrics = undefined; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /js/image_helper.js: -------------------------------------------------------------------------------- 1 | var image_helper = { 2 | crop: { 3 | as_square: (src, size, orig_width, orig_height) => { 4 | return new Promise(resolve => { 5 | let image = document.createElement("img"); 6 | image.addEventListener("load", () => { 7 | let canvas = document.createElement("canvas"); 8 | canvas.width = size; 9 | canvas.height = size; 10 | let ctx = canvas.getContext("2d"); 11 | 12 | square_size = Math.min(orig_width,orig_height); 13 | multiplier = 256/square_size; 14 | max_size = Math.max(orig_width,orig_height); 15 | crop_size = (max_size-square_size)/2; 16 | is_from_left = square_size == orig_height; 17 | if(is_from_left) { 18 | ctx.drawImage(image, -crop_size*multiplier, 0, max_size*multiplier, 256); 19 | } else { 20 | ctx.drawImage(image, 0, -crop_size*multiplier, 256, max_size*multiplier); 21 | } 22 | resolve(canvas.toDataURL()); 23 | }); 24 | image.src = src; 25 | }); 26 | } 27 | }, 28 | data_uri: { 29 | from_image_uri: async (uri, type="jpg") => { 30 | let image_content = await fetch(uri); 31 | let image_content_as_array_buffer = await image_content.arrayBuffer(); 32 | return "data:image/"+ type + ";base64," + Buffer.from(image_content_as_array_buffer).toString("base64") 33 | } 34 | }, 35 | dominant_color: (uri) => { 36 | // Vibrant Library 37 | if(Vibrant != null) { 38 | return new Promise(async (res) => { 39 | try { 40 | let vib = new Vibrant(uri); 41 | let palette = await vib.getPalette(); 42 | res((palette.Vibrant ?? 43 | palette.Muted ?? 44 | palette.DarkVibrant ?? 45 | palette.LightVibrant ?? 46 | palette.DarkMuted ?? 47 | palette.LightMuted)?.getHex() ?? "#777777"); 48 | } catch { 49 | res("#777777"); 50 | } 51 | }); 52 | }; 53 | // Browser implementation 54 | return new Promise((res) => { 55 | let the_image = document.createElement("img"); 56 | the_image.addEventListener("load", () => { 57 | let canvas = document.createElement("canvas"); 58 | canvas.width = 1; 59 | canvas.height = 1; 60 | let ctx = canvas.getContext("2d"); 61 | ctx.drawImage(the_image, 0,0,1,1); 62 | let dominant_pixel = ctx.getImageData(0,0,1,1); 63 | let dominant_color = "#"+Array.from(dominant_pixel.data).map(data=>data.toString(16).padStart(2, "0")).join(""); 64 | res(dominant_color.slice(0,7)); // remove alpha from color 65 | }); 66 | the_image.src = uri; 67 | }); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "trends": { 3 | "en": "Trends", 4 | "tr": "Popüler" 5 | }, 6 | "feed": { 7 | "en": "Feed", 8 | "tr": "Akış" 9 | }, 10 | "following": { 11 | "en": "Following", 12 | "tr": "Takip Edilenler" 13 | }, 14 | "playlists": { 15 | "en": "Playlists", 16 | "tr": "Oynatma Listeleri" 17 | }, 18 | "history": { 19 | "en": "History", 20 | "tr": "Geçmiş" 21 | }, 22 | "downloads": { 23 | "en": "Downloads", 24 | "tr": "İndirilenler" 25 | }, 26 | "settings": { 27 | "en": "Settings", 28 | "tr": "Ayarlar" 29 | }, 30 | "about_n_faq": { 31 | "en": "About & FAQ", 32 | "tr": "Hakkında & SSS" 33 | }, 34 | "devtools": { 35 | "en": "Devtools", 36 | "tr": "Geliştirici Araçları" 37 | }, 38 | "playback_speed": { 39 | "en": "Playback Speed", 40 | "tr": "Oynatma Hızı" 41 | }, 42 | "normal": { 43 | "en": "Normal", 44 | "tr": "Normal" 45 | }, 46 | "loading": { 47 | "en": "Loading..", 48 | "tr": "Hazırlanıyor.." 49 | }, 50 | "warning_not_family_safe_video": { 51 | "en": "It is not a family safe video. Do you want to watch it?", 52 | "tr": "Bu video aile ortamına uygun değil. İzlemek istediğinize emin misin?" 53 | }, 54 | "followers": { 55 | "en": "Followers", 56 | "tr": "Takipçi" 57 | }, 58 | "auto_add_to_queue": { 59 | "en": "Automatically add to queue", 60 | "tr": "Kendiliğinden sıraya ekle" 61 | }, 62 | "video_quality": { 63 | "en": "Video Quality", 64 | "tr": "Video Kalitesi" 65 | }, 66 | "audio_quality": { 67 | "en": "Audio Quality", 68 | "tr": "Ses Kalitesi" 69 | }, 70 | "audio_pitch": { 71 | "en": "Audio Patch", 72 | "tr": "Ses Perdesi" 73 | }, 74 | "captions": { 75 | "en": "Captions", 76 | "tr": "Alt Yazılar" 77 | }, 78 | "filters": { 79 | "en": "Filters", 80 | "tr": "Filtreler" 81 | }, 82 | "disable": { 83 | "en": "Disable", 84 | "tr": "Devre Dışı" 85 | }, 86 | "disabled_while_debug_mode": { 87 | "en": "Disabled while debug mode", 88 | "tr": "Hata ayıklama modunda devre dışı bırakıldı" 89 | }, 90 | "share": { 91 | "en": "Share", 92 | "tr": "Paylaş" 93 | }, 94 | "timelines": { 95 | "en": "Timelines", 96 | "tr": "Zaman Damgaları" 97 | }, 98 | "kaomoji_dog": { 99 | "en": "∪・ェ・∪" 100 | }, 101 | "kaomoji_bird": { 102 | "en": "(。・ө・。)" 103 | }, 104 | "there_are_nothing_more_dog": { 105 | "en": "There're nothing more than a dog that wants play with you", 106 | "tr": "Seninle oyun oynamak isteyen bir köpek dışında bir şey yok" 107 | }, 108 | "no_network_bird": { 109 | "en": "There're a bird who waits network to fly", 110 | "tr": "Burada uçabilmek için interneti bekleyen bir kuş var" 111 | }, 112 | "brightness": { 113 | "en": "Brightness", 114 | "tr": "Parlaklık" 115 | }, 116 | "contrast": { 117 | "en": "Contrast", 118 | "tr": "Karşıtlık" 119 | }, 120 | "blur": { 121 | "en": "Blur", 122 | "tr": "Bulanıklaştırma" 123 | }, 124 | "grayscale": { 125 | "en": "Grayscale", 126 | "tr": "Gri Tonlama" 127 | }, 128 | "invert": { 129 | "en": "Invert", 130 | "tr": "Ters Çevirme" 131 | }, 132 | "hue_rotate": { 133 | "en": "Hue Rotate", 134 | "tr": "Ton Değiştirme" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /css/pip.css: -------------------------------------------------------------------------------- 1 | @import url(./background-material.css); 2 | 3 | body { 4 | background: #000000; 5 | } 6 | 7 | * { 8 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 9 | user-select: none; 10 | } 11 | 12 | .control { 13 | -webkit-app-region: none; 14 | opacity: 0.05; 15 | color: #222222; 16 | background: #DDDDDD; 17 | z-index: 1; 18 | --size: 24px; 19 | padding: 4px; 20 | border-radius: 8px; 21 | border: none; 22 | cursor: pointer; 23 | transition-duration: 200ms; 24 | 25 | &:hover { 26 | opacity: 1; 27 | } 28 | 29 | &.close { 30 | position: fixed; 31 | top: 20px; 32 | right: 20px; 33 | } 34 | 35 | &.sound { 36 | position: fixed; 37 | bottom: 20px; 38 | left: 20px; 39 | } 40 | 41 | &.fullscreen { 42 | position: fixed; 43 | bottom: 20px; 44 | right: 20px; 45 | 46 | @media (max-width: 110px) { 47 | display: none; 48 | } 49 | } 50 | 51 | @media (max-width: 350px) { 52 | &.backward { 53 | display: none; 54 | } 55 | 56 | &.forward { 57 | display: none; 58 | } 59 | } 60 | 61 | @media (max-width: 250px) { 62 | &.previous { 63 | display: none; 64 | } 65 | 66 | &.next { 67 | display: none; 68 | } 69 | } 70 | 71 | @media (max-width: 150px) { 72 | &.sound { 73 | display: none; 74 | } 75 | &.fullscreen { 76 | position: unset; 77 | } 78 | } 79 | } 80 | 81 | .main-controls { 82 | position: fixed; 83 | bottom: 20px; 84 | left: 20px; 85 | right: 20px; 86 | display: flex; 87 | align-items: center; 88 | justify-content: center; 89 | gap: 5px; 90 | 91 | &:hover .control:not(:hover) { 92 | opacity: 0.2; 93 | transition-duration: 50ms; 94 | } 95 | 96 | @media (max-height: 110px) { 97 | display: none; 98 | } 99 | } 100 | 101 | .time-slider { 102 | -webkit-app-region: none; 103 | box-sizing: border-box; 104 | accent-color: white; 105 | opacity: 0.05; 106 | position: fixed; 107 | bottom: 60px; 108 | right: 20px; 109 | left: 20px; 110 | transition-duration: 200ms; 111 | 112 | &:hover { 113 | opacity: 1; 114 | } 115 | 116 | @media (max-height: 170px) { 117 | display: none; 118 | } 119 | } 120 | 121 | .time-info { 122 | position: fixed; 123 | color: #ffffff; 124 | bottom: 90px; 125 | left: 40px; 126 | opacity: 0; 127 | -webkit-app-region: drag; 128 | transition-duration: 200ms; 129 | 130 | :is(body):has(.time-slider:hover) & { 131 | opacity: 1; 132 | } 133 | 134 | @media (max-height: 200px) { 135 | display: none; 136 | } 137 | 138 | @media (max-width: 300px) { 139 | display: none; 140 | } 141 | 142 | } 143 | 144 | .video-container { 145 | z-index: -1; 146 | 147 | & video { 148 | -webkit-app-region: drag; 149 | position: fixed; 150 | top: 0; 151 | left: 0; 152 | width: 100%; 153 | height: 100%; 154 | 155 | &::cue { 156 | background: color-mix(in srgb, #222222 75%, transparent); 157 | color: #DDDDDD; 158 | } 159 | } 160 | } 161 | 162 | input[type="range"] { 163 | height: 12px; 164 | appearance: none; 165 | background: transparent; 166 | border-radius: 5px; 167 | clip-path: inset(0 round 10px); 168 | } 169 | 170 | input[type="range"]::-webkit-slider-runnable-track { 171 | width: 100%; 172 | background: #222222; 173 | height: 12px; 174 | border-radius: 5px; 175 | } 176 | 177 | input[type="range"]::-webkit-slider-thumb { 178 | appearance: none; 179 | height: 12px; 180 | width: 12px; 181 | background: #cccccc; 182 | box-shadow: -100vw 0 0 calc(100vw - 8px) #888888; 183 | } -------------------------------------------------------------------------------- /js/database.js: -------------------------------------------------------------------------------- 1 | var database = { 2 | following: new JsonDatabase("./dbs/following.json", {defaultStructure: []}), 3 | feed: new JsonDatabase("./dbs/feed.json"), 4 | playlists: new JsonDatabase("./dbs/playlists.json", { 5 | defaultStructure: { 6 | $liked_videos: { 7 | title: "Liked Videos", 8 | thumbnail: "./assets/inner/playlist_liked.png", 9 | list: [] 10 | }, 11 | $watch_later: { 12 | title: "Watch Later", 13 | thumbnail: "./assets/inner/playlist_watch_later.png", 14 | list: [] 15 | } 16 | }, 17 | fill_missing: true 18 | }), 19 | history: new JsonDatabase("./dbs/history.json", {defaultStructure: []}), 20 | settings: new JsonDatabase("./dbs/settings.json", {defaultStructureFrom: "./dbs/settings.template.json", fill_missing: true}) 21 | }; 22 | 23 | database.following.init(); 24 | database.following.add = (owner) => { 25 | /* 26 | * owner.id 27 | * owner.thumbnail (data-uri) 28 | * owner.name 29 | * owner.followers 30 | * owner.verified 31 | * owner.follow_time (ISODate) 32 | */ 33 | if(owner.id == null) return false; 34 | database.following.content.push({ 35 | id: owner.id, 36 | thumbnail: owner.thumbnail ?? null, 37 | name: owner.name ?? "Unknown Channel", 38 | followers: owner.followers ?? 0, 39 | verified: owner.verified ?? false, 40 | follow_time: (new Date).toISOString() 41 | }); 42 | database.following.update_file(); 43 | }; 44 | database.following.is_following = (owner_id) => { 45 | return database.following.content.filter(owner => owner.id == owner_id).length >= 1; 46 | }; 47 | database.following.remove = (owner_id) => { 48 | database.following.content = database.following.content.filter(owner => owner.id != owner_id); 49 | database.following.update_file(); 50 | }; 51 | database.feed.init(); 52 | database.feed.fetch = async () => { 53 | database.feed.content.fetch_time = (new Date).toISOString(); 54 | database.feed.content.feed = []; 55 | let owners = database.following.content.map(owner => owner.id); 56 | for(let owner_index=0; owner_index < owners.length; owner_index++){ 57 | let id = owners[owner_index]; 58 | database.feed.content.feed = [...database.feed.content.feed, ...(await yt_extractor.owner.get_owner_videos(id)).entry]; 59 | }; 60 | database.feed.content.feed.sort((a, b) => { 61 | if(new Date(a.published) < new Date(b.published)){ 62 | return 1; 63 | } else { 64 | return -1; 65 | } 66 | }); 67 | database.feed.update_file(); 68 | }; 69 | database.playlists.init(); 70 | database.history.init().then(() => { 71 | if (database.history.content.length > 2500) { 72 | console.warn("Huge watch history, Maybe you should clear or reduce the history?"); 73 | } 74 | }); 75 | database.history.add = (video) => { 76 | /* 77 | * video.id 78 | * video.owner_name 79 | * video.thumbnail (data-uri) 80 | * video.length 81 | * video.title 82 | * video.view_count (Counts of this user view count) 83 | * video.last_watch_time (ISODate) 84 | */ 85 | if(video.id == null) return false; 86 | let previous_record = database.history.content.filter($video => $video.id == video.id)?.[0]; 87 | let view_count = (previous_record?.view_count ?? 0) + 1; 88 | database.history.content = database.history.content.filter($video => $video.id != video.id); 89 | database.history.content.push({ 90 | id: video.id, 91 | owner_name: video.owner_name ?? "Unknown Owner", 92 | length: video.length ?? 0, 93 | thumbnail: video.thumbnail ?? null, 94 | title: video.title ?? "Unknown Title", 95 | view_count: view_count ?? 1, 96 | last_watch_time: (new Date).toISOString() 97 | }); 98 | database.history.update_file(); 99 | }; 100 | database.history.reduce = () => { 101 | let total_watch_count = database.history.content.reduce((total, video) => total + video.view_count, 0); 102 | let average_watch_count = total_watch_count / database.history.content.length; 103 | database.history.content = database.history.content.filter(video => video.view_count > average_watch_count); 104 | database.history.update_file(); 105 | }; 106 | database.settings.init(); 107 | -------------------------------------------------------------------------------- /assets/newflow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 17 | 20 | 23 | 24 | 25 | 34 | 39 | 41 | 44 | 47 | 51 | 52 | 55 | 59 | 60 | 63 | 67 | 68 | 71 | 75 | 76 | 79 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /js/pip.js: -------------------------------------------------------------------------------- 1 | var components = { 2 | $_human_readable_time: time => `${(time>=3600)?Math.floor(time/3600)+":":""}${(String(Math.floor(time/60)%60).length==1 && time>=3600)?"0":""}${Math.floor(time/60)%60}:${String(Math.floor(time%60)).padStart(2,"0")}`, 3 | $_window: null, 4 | controls: { 5 | close: document.querySelector(".close"), 6 | time_slider: document.querySelector(".time-slider"), 7 | sound: document.querySelector(".sound"), 8 | previous: document.querySelector(".previous"), 9 | backward: document.querySelector(".backward"), 10 | play_pause: document.querySelector(".play-pause"), 11 | forward: document.querySelector(".forward"), 12 | next: document.querySelector(".next"), 13 | fullscreen: document.querySelector(".fullscreen"), 14 | time_info: { 15 | current_time: document.querySelector(".current-time"), 16 | duration: document.querySelector(".duration") 17 | } 18 | }, 19 | video: null, 20 | __interval: setInterval(() => { 21 | let video_component = document.querySelector("video"); 22 | if(!!video_component) { 23 | console.log("Video conntected to PiP"); 24 | components.video = video_component; 25 | clearInterval(components.__interval); 26 | components.$_add_temp_listeners(); 27 | }; 28 | }, 100), 29 | __abort_controller: new AbortController(), 30 | $_add_temp_listeners: () => { 31 | components.video.addEventListener("pause", () => { 32 | components.controls.play_pause.innerText = "play_arrow"; 33 | }, {signal: components.__abort_controller.signal}); 34 | components.video.addEventListener("play", () => { 35 | components.controls.play_pause.innerText = "pause"; 36 | }, {signal: components.__abort_controller.signal}); 37 | components.controls.play_pause.innerText = ["pause","play_arrow"][Number(components.video.paused)]; 38 | 39 | components.video.addEventListener("durationchange", () => { 40 | time = components.video.duration; 41 | components.controls.time_slider.max = time; 42 | components.controls.time_info.duration.innerText = components.$_human_readable_time(time); 43 | }, {signal: components.__abort_controller.signal}); 44 | components.video.dispatchEvent(new Event("durationchange")); 45 | 46 | components.video.addEventListener("timeupdate", () => { 47 | let time = components.video.currentTime; 48 | components.controls.time_slider.value = time; 49 | components.controls.time_info.current_time.innerText = components.$_human_readable_time(time); 50 | }, {signal: components.__abort_controller.signal}); 51 | 52 | components.video.$$$video.$audio.addEventListener("volumechange", () => { 53 | components.controls.sound.innerText = ["volume_up", "volume_off"][Number(components.video.$$$video.$muted())]; 54 | }, {signal: components.__abort_controller.signal}); 55 | components.video.$$$video.$audio.dispatchEvent(new Event("volumechange")); 56 | } 57 | }; 58 | 59 | components.controls.close.addEventListener("click", () => { 60 | window.close(); 61 | }); 62 | 63 | components.controls.sound.addEventListener("click", () => { 64 | components.video.$$$video.$_mute(); 65 | }); 66 | components.controls.previous.addEventListener("click", () => { 67 | components.video.$$$video.$_previous(); 68 | }); 69 | components.controls.backward.addEventListener("click", () => { 70 | components.video.$$$video.$_seekby(-5); 71 | }); 72 | components.controls.play_pause.addEventListener("click", () => { 73 | components.video.$$$video.$_play_pause(); 74 | }); 75 | components.controls.forward.addEventListener("click", () => { 76 | components.video.$$$video.$_seekby(5); 77 | }); 78 | components.controls.next.addEventListener("click", () => { 79 | components.video.$$$video.$_next(); 80 | }); 81 | components.controls.time_slider.addEventListener("input", () => { 82 | components.video.currentTime = components.controls.time_slider.value; 83 | }); 84 | 85 | electron_loaded = () => { 86 | components.$_window = electron.BrowserWindow.getAllWindows().filter(_window => _window.getTitle() == document.title)[0]; 87 | components.controls.fullscreen.addEventListener("click", () => { 88 | if(components.$_window.isFullScreen()) { 89 | components.$_window.setFullScreen(false); 90 | components.controls.fullscreen.innerText = "fullscreen"; 91 | } else { 92 | components.$_window.setFullScreen(true); 93 | components.controls.fullscreen.innerText = "fullscreen_exit"; 94 | } 95 | }); 96 | 97 | // Set always on top (not works @ Linux/Wayland) 98 | components.$_window.setAlwaysOnTop(true); 99 | } 100 | 101 | // Add event listeners to abort all event listeners 102 | 103 | window.onunload = () => { 104 | components.__abort_controller.abort(); 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NewFlow ![Download Count](https://img.shields.io/github/downloads/malisipi/NewFlow/total?style=plastic&link=https%3A%2F%2Fgithub.com%2Fmalisipi%2FNewFlow) 2 | 3 | > [!IMPORTANT] 4 | > This application is no longer maintained or updated. And at the current state, failing in a lot of task. Please consider switching to alternative clients. Thanks for anybody who supported the application within the application lifespan (>1 year). It's the end of a story. 5 | > 6 | > **Why?** Because of reversing API and making them always keep alive possible is not possible when you have not a maintainer for doing that. And spending time with that is like wasting your time to make alive your application. You will never know when the API will be changed; and when API changed, your application fails. It's like a cat/mouse game, however the mouse just gave up. 7 | 8 | !["NewFlow Screenshot"](./assets/screenshots/4.png) 9 | 10 | Your free video player client for YouTube™. 11 | 12 | > You can see more screenshots from [Gallery](./assets/screenshots/GALLERY.md) 13 | 14 | ## Installation 15 | 16 | > [!IMPORTANT] 17 | > Installers is designed to help end users while installing NewFlow however the installers can have some bugs. If you encounter if any bug, please report it. 18 | 19 | ### For Windows 10 and above 20 | 21 | > You can use the installer that placed in releases section. 22 | 23 | ### For Linux 24 | 25 | > You can run the command that placed below to install NewFlow. 26 | 27 | > [!IMPORTANT] 28 | > `git` and `electron` commands must be installed to use the script. 29 | 30 | ```curl https://raw.githubusercontent.com/malisipi/NewFlow/main/linux-setup | bash``` 31 | 32 | ## Licenses 33 | 34 | > [!WARNING] 35 | >

Disclaimer

36 | >
    37 | >
  • This project is intended to demonstrate technical concepts and is not designed or endorsed for production or commercial use.
  • 38 | >
  • This software is provided "as-is" without warranty of any kind, express or implied. The authors are not liable for any misuse, damage, or legal consequences resulting from the use of this software.
  • 39 | >
40 | 41 | > [!IMPORTANT] 42 | > The application is licensed by [Apache 2.0 License](./LICENSE). 43 | > 44 | > The icons and banners of application is licensed by [Attribution-NonCommercial-NoDerivatives 4.0 International License](./LICENSE_CC_BY_NC_ND_4.0). So `./assets/banner.png`, `./assets/newflow.png`, `./assets/newflow.ico` and `./assets/newflow.svg` is licensed by [Attribution-NonCommercial-NoDerivatives 4.0 International License](./LICENSE_CC_BY_NC_ND_4.0) 45 | 46 | - [Electron.JS](https://github.com/electron/electron) is licensed by [MIT License](https://github.com/electron/electron/blob/main/LICENSE). 47 | - [electron/remote](https://github.com/electron/remote) is licensed by [MIT License](https://github.com/electron/remote/blob/main/LICENSE). 48 | - Module placed in `./node_modules/@electron/remote`. 49 | - [Yaru Icons](https://github.com/ubuntu/yaru) is licensed by [CC-BY-SA 4.0 License](https://github.com/ubuntu/yaru#copying-or-reusing). 50 | - Icons placed in `./assets/yaru-icons/`. 51 | - [Breeze Icons](https://github.com/KDE/breeze-icons) is licensed by [LGPL 2.1 License](https://github.com/KDE/breeze-icons/blob/master/COPYING.LIB). 52 | - Icons placed in `./assets/breeze-icons/`. 53 | - [Fluent Icons](https://github.com/microsoft/fluentui-system-icons) is licensed by [MIT License](https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE). 54 | - Icons placed in `./assets/fluent-icons/`. 55 | - [Bootstrap Icons](https://github.com/twbs/icons) is licensed by [MIT License](https://github.com/twbs/icons/blob/main/LICENSE). 56 | - Icons placed in `./assets/bootstrap-icons/`. 57 | - [Material Symbols](https://github.com/google/material-design-icons) is licensed by [Apache 2.0 License](https://github.com/google/material-design-icons/blob/master/LICENSE). 58 | - Icons placed in `./assets/material-symbols/`, `./fonts/material-symbols-rounded.woff2` and `./css/material-symbols.css` (Edited). 59 | - [yt-extractor.js](https://github.com/malisipi/yt-extractor.js) is licensed by [Apache 2.0 License](https://github.com/malisipi/yt-extractor.js/blob/main/LICENSE). 60 | - Sub dependencies: 61 | - [fast-xml-parse](https://www.npmjs.com/package/fast-xml-parser) is licensed by [MIT License](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/LICENSE). 62 | - [node-vibrant](https://github.com/Vibrant-Colors/node-vibrant) is licensed by [MIT License](https://github.com/Vibrant-Colors/node-vibrant/blob/master/LICENSE.md). 63 | - Script file placed in `./js/thirdparty/vibrant.js`. 64 | - [hls.js](https://github.com/video-dev/hls.js) is licensed by [Apache 2.0 License](https://github.com/video-dev/hls.js/blob/master/LICENSE). 65 | - Script file placed in `./js/thirdparty/hls.light.min.js`. 66 | 67 | ## FAQ 68 | 69 | ### Picture-in-Picture gets under windows on Linux/Wayland 70 | 71 | Since Wayland protocol doesn't support to set windows always on top, there're no possible way to do it without a Window-Manager trick. 72 | 73 | * If you're using KWin Windows Manager (Default Windows Manager of KDE Desktop Environment), you can set a new window rule to keep on top. 74 | 75 | * If you're using Ubuntu or based distro, you can look up [this extension](https://github.com/Rafostar/gnome-shell-extension-pip-on-top) to get working. 76 | 77 | ### Creating Desktop Shortcut on Linux with Wayland Support 78 | 79 | ``` 80 | NEWFLOW_FLAGS="--enable-features=UseOzonePlatform --ozone-platform=wayland" ./scripts/install-desktop-file 81 | ``` 82 | -------------------------------------------------------------------------------- /windows-setup/setup.py: -------------------------------------------------------------------------------- 1 | setup_version = "0.0.3"; 2 | 3 | import urllib.request; 4 | from zipfile import ZipFile; 5 | import tempfile; 6 | import os; 7 | import os.path; 8 | import shutil; 9 | import webbrowser; 10 | import subprocess; 11 | import sys; 12 | import tkinter; 13 | from tkinter import ttk; 14 | 15 | temp_dir = tempfile.gettempdir()+"\\"; 16 | newflow_download = "https://github.com/malisipi/NewFlow/archive/refs/heads/main.zip"; 17 | electron_download = "https://github.com/electron/electron/releases/download/v31.4.0/electron-v31.4.0-win32-x64.zip"; 18 | ytextratorjs_download = "https://github.com/malisipi/yt-extractor.js/archive/refs/heads/main.zip"; 19 | electron_ver = "v31.4.0"; 20 | 21 | def read_license(): 22 | webbrowser.open("https://www.apache.org/licenses/LICENSE-2.0.txt"); 23 | 24 | def open_website(): 25 | webbrowser.open("https://github.com/malisipi/NewFlow/"); 26 | 27 | def update_state(info): 28 | state_text["text"] = info; 29 | show_progress(0,0,1); 30 | setup_window.update(); 31 | 32 | def show_progress(block_num, block_size, total_size): 33 | global state 34 | if(total_size<1): 35 | if(state_progress["mode"] != "indeterminate"): 36 | state_progress["mode"] = "indeterminate"; 37 | state_progress["value"]=(block_num%250)/250; 38 | else: 39 | if(state_progress["mode"] != "determinate"): 40 | state_progress["mode"] = "determinate"; 41 | state_progress["value"]=int((block_num*block_size*100)/total_size); 42 | setup_window.update(); 43 | 44 | def newflow_install(): 45 | update_state("Downloading Electron Launcher..."); 46 | if(not os.path.exists(temp_dir + "electron" + electron_ver + ".zip")): 47 | urllib.request.urlretrieve(electron_download, temp_dir + "electron" + electron_ver + ".zip", show_progress); 48 | 49 | update_state("Downloading NewFlow..."); 50 | urllib.request.urlretrieve(newflow_download, temp_dir + "newflow.zip", show_progress); 51 | 52 | update_state("Downloading yt-extractor..."); 53 | urllib.request.urlretrieve(ytextratorjs_download, temp_dir + "ytextractor.zip", show_progress); 54 | 55 | update_state("Extracting Electron Launcher..."); 56 | zip_archive = ZipFile(temp_dir + "electron" + electron_ver + ".zip"); 57 | zip_archive.extractall("C:/Programs/NewFlow/"); 58 | os.remove("C:/Programs/NewFlow/resources/default_app.asar"); 59 | 60 | update_state("Extracting NewFlow..."); 61 | zip_archive = ZipFile(temp_dir + "newflow.zip"); 62 | zip_archive.extractall("C:/Programs/NewFlow/resources/"); 63 | if(os.path.exists("C:/Programs/NewFlow/resources/app")): 64 | if(os.path.exists("C:/Programs/NewFlow/resources/app/dbs")): 65 | if(os.path.exists("C:/Programs/NewFlow/__dbs_backup")): 66 | shutil.rmtree("C:/Programs/NewFlow/__dbs_backup"); 67 | os.rename("C:/Programs/NewFlow/resources/app/dbs","C:/Programs/NewFlow/__dbs_backup"); 68 | shutil.rmtree("C:/Programs/NewFlow/resources/app"); 69 | os.rename("C:/Programs/NewFlow/resources/NewFlow-main","C:/Programs/NewFlow/resources/app"); 70 | if(os.path.exists("C:/Programs/NewFlow/__dbs_backup")): 71 | os.rename("C:/Programs/NewFlow/__dbs_backup","C:/Programs/NewFlow/resources/app/dbs"); 72 | else: 73 | os.rename("C:/Programs/NewFlow/resources/NewFlow-main","C:/Programs/NewFlow/resources/app"); 74 | if(not os.path.exists("C:/Programs/NewFlow/resources/app/dbs")): 75 | os.mkdir("C:/Programs/NewFlow/resources/app/dbs"); 76 | 77 | update_state("Extracting yt-extractor..."); 78 | zip_archive = ZipFile(temp_dir + "ytextractor.zip"); 79 | zip_archive.extractall("C:/Programs/NewFlow/resources/app/"); 80 | if(os.path.exists("C:/Programs/NewFlow/resources/app/yt-extractor")): 81 | shutil.rmtree("C:/Programs/NewFlow/resources/app/yt-extractor"); 82 | os.rename("C:/Programs/NewFlow/resources/app/yt-extractor.js-main","C:/Programs/NewFlow/resources/app/yt-extractor"); 83 | 84 | update_state("Creating Shortcuts"); 85 | subprocess.run(["wscript", "C:/Programs/NewFlow/resources/app/scripts/create_launcher.vbs"]); 86 | 87 | update_state("Finished!"); 88 | show_progress(1,1,1); 89 | 90 | def clear_cache(): 91 | if(os.path.exists(temp_dir + "electron" + electron_ver + ".zip")): 92 | os.remove(temp_dir + "electron" + electron_ver + ".zip"); 93 | if(os.path.exists(temp_dir + "newflow.zip")): 94 | os.remove(temp_dir + "newflow.zip"); 95 | if(os.path.exists(temp_dir + "ytextractor.zip")): 96 | os.remove(temp_dir + "ytextractor.zip"); 97 | 98 | setup_window = tkinter.Tk(); 99 | setup_window.title("NewFlow - Setup " + setup_version); 100 | setup_window.resizable(False, False); 101 | setup_window.geometry('400x200'); 102 | 103 | state_text = tkinter.Label(setup_window, text="Setup is ready!", justify="center"); 104 | state_text.place(x=60, y=40, width=280, height=25); 105 | state_progress = ttk.Progressbar(setup_window, length=100); 106 | state_progress.place(x=60, y=80, width=280, height=25); 107 | install_button = ttk.Button(setup_window, text='Install NewFlow', command=newflow_install); 108 | install_button.place(x=100, y=120, width=200, height=25); 109 | website_button = ttk.Button(setup_window, text='Open Website', command=open_website); 110 | website_button.place(x=20, y=155, width=100, height=25); 111 | license_button = ttk.Button(setup_window, text='Read License', command=read_license); 112 | license_button.place(x=280, y=155, width=100, height=25); 113 | clear_cache_button = ttk.Button(setup_window, text='Clear Cache', command=clear_cache); 114 | clear_cache_button.place(x=150, y=155, width=100, height=25); 115 | 116 | setup_window.mainloop(); 117 | -------------------------------------------------------------------------------- /css/layout.css: -------------------------------------------------------------------------------- 1 | html { 2 | --titlebar-height: 30px; 3 | --titlebar-button-width: 50px; 4 | --sidebar-width: 50px; 5 | --playbar-height: 50px; 6 | --playbar-spacing: 10px; 7 | } 8 | 9 | body[os="linux"] { 10 | --titlebar-button-width: 30px; 11 | } 12 | 13 | * { /* Clear CSS */ 14 | box-sizing: border-box; 15 | margin: 0; 16 | padding: 0; 17 | user-select: none; 18 | } 19 | 20 | div.titlebar { 21 | display: flex; 22 | left: 0; 23 | top: 0; 24 | width: 100%; 25 | height: var(--titlebar-height); 26 | -webkit-app-region: drag; 27 | 28 | & .button-layout { 29 | display: flex; 30 | 31 | &[custom] { 32 | & button:not([show]) { 33 | display: none; 34 | } 35 | } 36 | 37 | :is(body[os="linux"]) &[variant="kde"] { 38 | & img:not(.kde) { 39 | display: none; 40 | } 41 | } 42 | 43 | :is(body[os="linux"]) &:not([variant]) { 44 | & img.kde { 45 | display: none; 46 | } 47 | } 48 | } 49 | 50 | & .title { 51 | padding-left: 10px; 52 | flex-grow: 1; 53 | align-self: center; 54 | } 55 | 56 | & input.search { 57 | position: fixed; 58 | left: 50%; 59 | top: 2.5px; 60 | height: calc(var(--titlebar-height) - 5px); 61 | width: 300px; 62 | transform: translate(-50%, 0); 63 | padding: 0 5px; 64 | -webkit-app-region: none; 65 | } 66 | 67 | & button { 68 | width: var(--titlebar-button-width); 69 | -webkit-app-region: none; 70 | 71 | &.menu { 72 | width: var(--sidebar-width); 73 | } 74 | 75 | &[state="maximize"] img.restore { 76 | display: none; 77 | } 78 | 79 | &[state="restore"] img.maximize { 80 | display: none; 81 | } 82 | 83 | & img { 84 | width: 16px; 85 | filter: brightness(0); 86 | :is(body):is([theme="dark"], [theme="black"]) & { 87 | filter: brightness(0) invert(1); 88 | } 89 | 90 | :is(body)[os="linux"] &.other { 91 | display: none; 92 | } 93 | 94 | :is(body)[os="other"] &.linux { 95 | display: none; 96 | } 97 | } 98 | } 99 | } 100 | 101 | @media (max-width: 700px) { 102 | .titlebar input.search { 103 | &:not(:focus) { 104 | pointer-events: none; 105 | opacity: 0; 106 | -webkit-app-region: drag; 107 | max-width: 0; 108 | } 109 | 110 | &:focus { 111 | width: 90%; 112 | left: 5%; 113 | transform: unset; 114 | } 115 | 116 | .titlebar:has(&:focus) *:not(&) { 117 | pointer-events: none; 118 | opacity: 0; 119 | -webkit-app-region: drag; 120 | max-width: 0; 121 | } 122 | } 123 | } 124 | 125 | div.sidenav { 126 | position: fixed; 127 | left: 0; 128 | top: var(--titlebar-height); 129 | height: calc(100% - var(--titlebar-height)); 130 | width: var(--sidebar-width); 131 | display: flex; 132 | flex-direction: column; 133 | background: inherit; 134 | z-index: 8; 135 | 136 | & > * { 137 | display: flex; 138 | align-items: center; 139 | gap: 5px; 140 | 141 | & > * { 142 | pointer-events: none; 143 | } 144 | } 145 | 146 | & material-symbol { 147 | width: var(--sidebar-width); 148 | height: var(--sidebar-width); 149 | } 150 | 151 | & :is(.open-only, .description) { 152 | display: none; 153 | } 154 | 155 | &[open] { 156 | width: 200px; 157 | 158 | & :is(.open-only, .description) { 159 | display: flex; 160 | } 161 | } 162 | 163 | & img { 164 | width: 100%; 165 | } 166 | 167 | @media (max-width: 500px) { 168 | &:not([open]) { 169 | pointer-events: none; 170 | opacity: 0; 171 | visibility: hidden; 172 | } 173 | } 174 | } 175 | 176 | button.global-action.search { 177 | position: fixed; 178 | bottom: 24px; 179 | right: 24px; 180 | width: var(--sidebar-width); 181 | height: var(--sidebar-width); 182 | display: none; 183 | z-index: 99999999; 184 | 185 | @media (max-width: 700px) { 186 | display: unset; 187 | } 188 | } 189 | 190 | div.tabs { 191 | & div.view { 192 | position: fixed; 193 | left: var(--sidebar-width); 194 | top: var(--titlebar-height); 195 | height: calc(100% - var(--titlebar-height)); 196 | width: calc(100% - var(--sidebar-width)); 197 | display: block; 198 | overflow: auto; 199 | padding: 10px; 200 | 201 | @media (max-width: 500px) { 202 | left: 0; 203 | width: 100%; 204 | } 205 | 206 | .tabs:not([active="trends"]) &:is(#trends), 207 | .tabs:not([active="search"]) &:is(#search), 208 | .tabs:not([active="watch"]) &:is(#watch), 209 | .tabs:not([active="feed"]) &:is(#feed), 210 | .tabs:not([active="following"]) &:is(#following), 211 | .tabs:not([active="owner"]) &:is(#owner), 212 | .tabs:not([active="playlists"]) &:is(#playlists), 213 | .tabs:not([active="playlist"]) &:is(#playlist), 214 | .tabs:not([active="history"]) &:is(#history), 215 | .tabs:not([active="downloads"]) &:is(#downloads), 216 | .tabs:not([active="settings"]) &:is(#settings), 217 | .tabs:not([active="about"]) &:is(#about), 218 | .tabs:not([active="offline"]) &:is(#offline), 219 | .tabs:not([active="queue"]) &:is(#queue) { 220 | display: none; 221 | } 222 | } 223 | } 224 | 225 | div.playbar { 226 | position: fixed; 227 | bottom: var(--playbar-spacing); 228 | left: 10%; 229 | width: 80%; 230 | height: var(--playbar-height); 231 | display: none; 232 | padding: 0 20px; 233 | grid-template-areas: 234 | "thumbnail title controls" 235 | "thumbnail owner controls"; 236 | grid-auto-columns: min-content auto min-content; 237 | align-items: center; 238 | gap: 0 10px; 239 | z-index: 7; 240 | 241 | & .thumbnail { 242 | grid-area: thumbnail; 243 | height: var(--playbar-height); 244 | } 245 | 246 | & .title { 247 | grid-area: title; 248 | text-overflow: ellipsis; 249 | overflow: hidden; 250 | text-wrap: nowrap; 251 | transform: translate(0, 2px); 252 | } 253 | 254 | & .owner { 255 | grid-area: owner; 256 | text-overflow: ellipsis; 257 | overflow: hidden; 258 | text-wrap: nowrap; 259 | transform: translate(0, -2px); 260 | } 261 | 262 | & .controls { 263 | display: flex; 264 | grid-area: controls; 265 | flex-wrap: nowrap; 266 | } 267 | 268 | @media (max-width: 600px) { 269 | left: 0; 270 | width: 100%; 271 | bottom: 0; 272 | } 273 | 274 | @media (max-width: 450px) { 275 | padding: 0 10px 0 0; 276 | } 277 | 278 | @media (max-width: 360px) { 279 | padding: 0 10px; 280 | 281 | & .thumbnail { 282 | display: none; 283 | } 284 | } 285 | } 286 | 287 | dialog#share[open] { 288 | display: flex; 289 | position: fixed; 290 | top: 50%; 291 | left: 50%; 292 | transform: translate(-50%, -50%); 293 | z-index: 999; 294 | gap: 10px; 295 | padding: 10px; 296 | flex-direction: column; 297 | align-items: center; 298 | width: min(80%, 400px); 299 | 300 | & .close { 301 | position: absolute; 302 | right: 10px; 303 | } 304 | 305 | & .actions { 306 | display: flex; 307 | gap: 5px; 308 | 309 | & div.masked-symbol { 310 | width: 32px; 311 | height: 32px; 312 | background: var(--text-color); 313 | mask-size: 32px !important; 314 | } 315 | 316 | & material-symbol { 317 | --size: 32px; 318 | } 319 | } 320 | 321 | *:is(body):has(&) > *:not(&, .titlebar), *:is(body):has(&) .titlebar > :not(.button-layout) { 322 | pointer-events: none; 323 | opacity: 0.3; 324 | } 325 | } 326 | 327 | :is(body):has(div.tabs:not([active="watch"]) #watch video[src*="://"]), 328 | :is(body):has(div.tabs:not([active="watch"])):not(:has(div.tabs #watch video)) { 329 | & div.tabs { 330 | & div.view { 331 | height: calc(100% - calc(var(--titlebar-height) + calc(var(--playbar-height) + calc(var(--playbar-spacing) * 1.5)))); 332 | } 333 | } 334 | 335 | & div.playbar { 336 | display: grid; 337 | } 338 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | NewFlow 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 34 | 35 | 36 | 37 |
38 | 39 |
NewFlow
40 | 41 |
42 | 47 | 55 | 60 |
61 |
62 |
63 | 64 |
65 | local_fire_department 66 |
67 |
68 |
69 | rss_feed 70 |
71 |
72 |
73 | tv 74 |
75 |
76 |
77 | bookmarks 78 |
79 |
80 |
81 | history 82 |
83 |
84 |
85 | download 86 |
87 |
88 |
89 | settings 90 |
91 |
92 |
93 | info 94 |
95 |
96 | 100 |
101 | 102 |
103 | 104 | 105 |
106 |
107 |
108 | 109 | 110 | 111 | 126 | 127 | 133 | 145 | 146 | 147 | 148 | 162 |
163 | 164 |
165 |
166 |
167 | skip_previous 168 | fast_rewind 169 | play_arrow 170 | fast_forward 171 | skip_next 172 |
173 | volume_up 174 | 175 |
176 |
0:00/0:00
177 |
178 |
179 | movie 180 | menu_book 181 | slow_motion_video 182 | closed_caption 183 | settings 184 | fullscreen 185 |
186 |
187 |
188 | 189 |
Stream will be started at
190 |
191 |
192 |
193 |
194 | 195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 | 203 | 204 | 205 | 206 | 207 |
208 |
209 |
210 |
Comments is not implemented
211 |
212 |
213 |
214 |
215 | 216 | 217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
Playlist
227 |
228 |
229 |
230 |
231 | 246 |
247 |
248 |
249 |
250 | 251 |
    252 |
  • 253 | Active Theme 254 | 255 |
  • 256 |
  • 257 | Background Material 258 | 259 |
  • 260 |
261 |
262 |
263 | 264 | Settings about media 265 |
266 |
267 | 268 | Settings about plugins 269 |
270 |
271 |
272 | NewFlow
273 | Developed by malisipi 274 |
275 |
276 |
277 |
278 | 279 |
280 |
281 |
282 |
283 | 284 |
285 |
286 |
287 | 288 | 289 | 290 |
291 |
292 | 293 | close 294 |
295 |
296 | 297 |
298 |
299 |
link
300 |
globe
301 |
302 |
303 | 304 | 305 | -------------------------------------------------------------------------------- /LICENSE_CC_BY_NC_ND_4.0: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-NoDerivatives 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 58 | International Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-NoDerivatives 4.0 International Public 63 | License ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Copyright and Similar Rights means copyright and/or similar rights 84 | closely related to copyright including, without limitation, 85 | performance, broadcast, sound recording, and Sui Generis Database 86 | Rights, without regard to how the rights are labeled or 87 | categorized. For purposes of this Public License, the rights 88 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 89 | Rights. 90 | 91 | c. Effective Technological Measures means those measures that, in the 92 | absence of proper authority, may not be circumvented under laws 93 | fulfilling obligations under Article 11 of the WIPO Copyright 94 | Treaty adopted on December 20, 1996, and/or similar international 95 | agreements. 96 | 97 | d. Exceptions and Limitations means fair use, fair dealing, and/or 98 | any other exception or limitation to Copyright and Similar Rights 99 | that applies to Your use of the Licensed Material. 100 | 101 | e. Licensed Material means the artistic or literary work, database, 102 | or other material to which the Licensor applied this Public 103 | License. 104 | 105 | f. Licensed Rights means the rights granted to You subject to the 106 | terms and conditions of this Public License, which are limited to 107 | all Copyright and Similar Rights that apply to Your use of the 108 | Licensed Material and that the Licensor has authority to license. 109 | 110 | g. Licensor means the individual(s) or entity(ies) granting rights 111 | under this Public License. 112 | 113 | h. NonCommercial means not primarily intended for or directed towards 114 | commercial advantage or monetary compensation. For purposes of 115 | this Public License, the exchange of the Licensed Material for 116 | other material subject to Copyright and Similar Rights by digital 117 | file-sharing or similar means is NonCommercial provided there is 118 | no payment of monetary compensation in connection with the 119 | exchange. 120 | 121 | i. Share means to provide material to the public by any means or 122 | process that requires permission under the Licensed Rights, such 123 | as reproduction, public display, public performance, distribution, 124 | dissemination, communication, or importation, and to make material 125 | available to the public including in ways that members of the 126 | public may access the material from a place and at a time 127 | individually chosen by them. 128 | 129 | j. Sui Generis Database Rights means rights other than copyright 130 | resulting from Directive 96/9/EC of the European Parliament and of 131 | the Council of 11 March 1996 on the legal protection of databases, 132 | as amended and/or succeeded, as well as other essentially 133 | equivalent rights anywhere in the world. 134 | 135 | k. You means the individual or entity exercising the Licensed Rights 136 | under this Public License. Your has a corresponding meaning. 137 | 138 | 139 | Section 2 -- Scope. 140 | 141 | a. License grant. 142 | 143 | 1. Subject to the terms and conditions of this Public License, 144 | the Licensor hereby grants You a worldwide, royalty-free, 145 | non-sublicensable, non-exclusive, irrevocable license to 146 | exercise the Licensed Rights in the Licensed Material to: 147 | 148 | a. reproduce and Share the Licensed Material, in whole or 149 | in part, for NonCommercial purposes only; and 150 | 151 | b. produce and reproduce, but not Share, Adapted Material 152 | for NonCommercial purposes only. 153 | 154 | 2. Exceptions and Limitations. For the avoidance of doubt, where 155 | Exceptions and Limitations apply to Your use, this Public 156 | License does not apply, and You do not need to comply with 157 | its terms and conditions. 158 | 159 | 3. Term. The term of this Public License is specified in Section 160 | 6(a). 161 | 162 | 4. Media and formats; technical modifications allowed. The 163 | Licensor authorizes You to exercise the Licensed Rights in 164 | all media and formats whether now known or hereafter created, 165 | and to make technical modifications necessary to do so. The 166 | Licensor waives and/or agrees not to assert any right or 167 | authority to forbid You from making technical modifications 168 | necessary to exercise the Licensed Rights, including 169 | technical modifications necessary to circumvent Effective 170 | Technological Measures. For purposes of this Public License, 171 | simply making modifications authorized by this Section 2(a) 172 | (4) never produces Adapted Material. 173 | 174 | 5. Downstream recipients. 175 | 176 | a. Offer from the Licensor -- Licensed Material. Every 177 | recipient of the Licensed Material automatically 178 | receives an offer from the Licensor to exercise the 179 | Licensed Rights under the terms and conditions of this 180 | Public License. 181 | 182 | b. No downstream restrictions. You may not offer or impose 183 | any additional or different terms or conditions on, or 184 | apply any Effective Technological Measures to, the 185 | Licensed Material if doing so restricts exercise of the 186 | Licensed Rights by any recipient of the Licensed 187 | Material. 188 | 189 | 6. No endorsement. Nothing in this Public License constitutes or 190 | may be construed as permission to assert or imply that You 191 | are, or that Your use of the Licensed Material is, connected 192 | with, or sponsored, endorsed, or granted official status by, 193 | the Licensor or others designated to receive attribution as 194 | provided in Section 3(a)(1)(A)(i). 195 | 196 | b. Other rights. 197 | 198 | 1. Moral rights, such as the right of integrity, are not 199 | licensed under this Public License, nor are publicity, 200 | privacy, and/or other similar personality rights; however, to 201 | the extent possible, the Licensor waives and/or agrees not to 202 | assert any such rights held by the Licensor to the limited 203 | extent necessary to allow You to exercise the Licensed 204 | Rights, but not otherwise. 205 | 206 | 2. Patent and trademark rights are not licensed under this 207 | Public License. 208 | 209 | 3. To the extent possible, the Licensor waives any right to 210 | collect royalties from You for the exercise of the Licensed 211 | Rights, whether directly or through a collecting society 212 | under any voluntary or waivable statutory or compulsory 213 | licensing scheme. In all other cases the Licensor expressly 214 | reserves any right to collect such royalties, including when 215 | the Licensed Material is used other than for NonCommercial 216 | purposes. 217 | 218 | 219 | Section 3 -- License Conditions. 220 | 221 | Your exercise of the Licensed Rights is expressly made subject to the 222 | following conditions. 223 | 224 | a. Attribution. 225 | 226 | 1. If You Share the Licensed Material, You must: 227 | 228 | a. retain the following if it is supplied by the Licensor 229 | with the Licensed Material: 230 | 231 | i. identification of the creator(s) of the Licensed 232 | Material and any others designated to receive 233 | attribution, in any reasonable manner requested by 234 | the Licensor (including by pseudonym if 235 | designated); 236 | 237 | ii. a copyright notice; 238 | 239 | iii. a notice that refers to this Public License; 240 | 241 | iv. a notice that refers to the disclaimer of 242 | warranties; 243 | 244 | v. a URI or hyperlink to the Licensed Material to the 245 | extent reasonably practicable; 246 | 247 | b. indicate if You modified the Licensed Material and 248 | retain an indication of any previous modifications; and 249 | 250 | c. indicate the Licensed Material is licensed under this 251 | Public License, and include the text of, or the URI or 252 | hyperlink to, this Public License. 253 | 254 | For the avoidance of doubt, You do not have permission under 255 | this Public License to Share Adapted Material. 256 | 257 | 2. You may satisfy the conditions in Section 3(a)(1) in any 258 | reasonable manner based on the medium, means, and context in 259 | which You Share the Licensed Material. For example, it may be 260 | reasonable to satisfy the conditions by providing a URI or 261 | hyperlink to a resource that includes the required 262 | information. 263 | 264 | 3. If requested by the Licensor, You must remove any of the 265 | information required by Section 3(a)(1)(A) to the extent 266 | reasonably practicable. 267 | 268 | 269 | Section 4 -- Sui Generis Database Rights. 270 | 271 | Where the Licensed Rights include Sui Generis Database Rights that 272 | apply to Your use of the Licensed Material: 273 | 274 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 275 | to extract, reuse, reproduce, and Share all or a substantial 276 | portion of the contents of the database for NonCommercial purposes 277 | only and provided You do not Share Adapted Material; 278 | 279 | b. if You include all or a substantial portion of the database 280 | contents in a database in which You have Sui Generis Database 281 | Rights, then the database in which You have Sui Generis Database 282 | Rights (but not its individual contents) is Adapted Material; and 283 | 284 | c. You must comply with the conditions in Section 3(a) if You Share 285 | all or a substantial portion of the contents of the database. 286 | 287 | For the avoidance of doubt, this Section 4 supplements and does not 288 | replace Your obligations under this Public License where the Licensed 289 | Rights include other Copyright and Similar Rights. 290 | 291 | 292 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 293 | 294 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 295 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 296 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 297 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 298 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 299 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 300 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 301 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 302 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 303 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 304 | 305 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 306 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 307 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 308 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 309 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 310 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 311 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 312 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 313 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 314 | 315 | c. The disclaimer of warranties and limitation of liability provided 316 | above shall be interpreted in a manner that, to the extent 317 | possible, most closely approximates an absolute disclaimer and 318 | waiver of all liability. 319 | 320 | 321 | Section 6 -- Term and Termination. 322 | 323 | a. This Public License applies for the term of the Copyright and 324 | Similar Rights licensed here. However, if You fail to comply with 325 | this Public License, then Your rights under this Public License 326 | terminate automatically. 327 | 328 | b. Where Your right to use the Licensed Material has terminated under 329 | Section 6(a), it reinstates: 330 | 331 | 1. automatically as of the date the violation is cured, provided 332 | it is cured within 30 days of Your discovery of the 333 | violation; or 334 | 335 | 2. upon express reinstatement by the Licensor. 336 | 337 | For the avoidance of doubt, this Section 6(b) does not affect any 338 | right the Licensor may have to seek remedies for Your violations 339 | of this Public License. 340 | 341 | c. For the avoidance of doubt, the Licensor may also offer the 342 | Licensed Material under separate terms or conditions or stop 343 | distributing the Licensed Material at any time; however, doing so 344 | will not terminate this Public License. 345 | 346 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 347 | License. 348 | 349 | 350 | Section 7 -- Other Terms and Conditions. 351 | 352 | a. The Licensor shall not be bound by any additional or different 353 | terms or conditions communicated by You unless expressly agreed. 354 | 355 | b. Any arrangements, understandings, or agreements regarding the 356 | Licensed Material not stated herein are separate from and 357 | independent of the terms and conditions of this Public License. 358 | 359 | 360 | Section 8 -- Interpretation. 361 | 362 | a. For the avoidance of doubt, this Public License does not, and 363 | shall not be interpreted to, reduce, limit, restrict, or impose 364 | conditions on any use of the Licensed Material that could lawfully 365 | be made without permission under this Public License. 366 | 367 | b. To the extent possible, if any provision of this Public License is 368 | deemed unenforceable, it shall be automatically reformed to the 369 | minimum extent necessary to make it enforceable. If the provision 370 | cannot be reformed, it shall be severed from this Public License 371 | without affecting the enforceability of the remaining terms and 372 | conditions. 373 | 374 | c. No term or condition of this Public License will be waived and no 375 | failure to comply consented to unless expressly agreed to by the 376 | Licensor. 377 | 378 | d. Nothing in this Public License constitutes or may be interpreted 379 | as a limitation upon, or waiver of, any privileges and immunities 380 | that apply to the Licensor or You, including from the legal 381 | processes of any jurisdiction or authority. 382 | 383 | ======================================================================= 384 | 385 | Creative Commons is not a party to its public 386 | licenses. Notwithstanding, Creative Commons may elect to apply one of 387 | its public licenses to material it publishes and in those instances 388 | will be considered the “Licensor.” The text of the Creative Commons 389 | public licenses is dedicated to the public domain under the CC0 Public 390 | Domain Dedication. Except for the limited purpose of indicating that 391 | material is shared under a Creative Commons public license or as 392 | otherwise permitted by the Creative Commons policies published at 393 | creativecommons.org/policies, Creative Commons does not authorize the 394 | use of the trademark "Creative Commons" or any other trademark or logo 395 | of Creative Commons without its prior written consent including, 396 | without limitation, in connection with any unauthorized modifications 397 | to any of its public licenses or any other arrangements, 398 | understandings, or agreements concerning use of licensed material. For 399 | the avoidance of doubt, this paragraph does not form part of the 400 | public licenses. 401 | 402 | Creative Commons may be contacted at creativecommons.org. 403 | 404 | -------------------------------------------------------------------------------- /js/render.js: -------------------------------------------------------------------------------- 1 | var render = { 2 | $: { 3 | video_preview: (mode="normal", video_info) => { /* mode=normal|compact */ 4 | /* video_info 5 | * thumbnail 6 | * owner.name 7 | * owner.verified 8 | * length 9 | * title 10 | * id 11 | */ 12 | let video = document.createElement("div"); 13 | video.className = mode + " -video-preview"; 14 | let thumbnail = document.createElement("img"); 15 | thumbnail.draggable = false; 16 | thumbnail.loading = "lazy"; 17 | thumbnail.src = video_info?.thumbnail ?? ""; 18 | video.append(thumbnail); 19 | let owner = document.createElement("span"); 20 | owner.className = "owner"; 21 | owner.innerText = video_info?.owner?.name ?? ""; 22 | owner.setAttribute("verified", video_info?.owner?.verified ?? false); 23 | video.append(owner); 24 | let length = document.createElement("span"); 25 | length.className = "length"; 26 | length.setAttribute("length", video_info?.length ?? 0); 27 | length.innerText = components.$_human_readable_time(video_info?.length ?? 0); 28 | video.append(length); 29 | let title = document.createElement("span"); 30 | title.className = "title"; 31 | title.innerText = video_info?.title; 32 | video.append(title); 33 | video.addEventListener("click", (_, _id=video_info?.id, keep_queue=video_info?.keep_queue) => { 34 | if(keep_queue != true){ 35 | components.tabs.watch.video.$_clear_queue(); 36 | } 37 | components.tabs.$_switch("watch"); 38 | render.watch(_id); 39 | }) 40 | return video; 41 | }, 42 | playlist_preview: (mode="normal", playlist_info) => { /* mode=normal|compact */ 43 | /* playlist_info 44 | * title 45 | * thumbnail 46 | * id 47 | */ 48 | let playlist = document.createElement("div"); 49 | playlist.className = mode + " -playlist-preview"; 50 | let thumbnail = document.createElement("img"); 51 | thumbnail.draggable = false; 52 | thumbnail.loading = "lazy"; 53 | thumbnail.src = playlist_info?.thumbnail ?? ""; 54 | playlist.append(thumbnail); 55 | let title = document.createElement("span"); 56 | title.className = "title"; 57 | title.innerText = playlist_info?.title; 58 | playlist.append(title); 59 | playlist.addEventListener("click", (_, _id=playlist_info?.id) => { 60 | alert(_id); 61 | }) 62 | return playlist; 63 | }, 64 | owner_preview: (mode="normal", owner_info) => { /* mode=normal|compact */ 65 | let owner = document.createElement("div"); 66 | owner.className = mode + " -owner-preview"; 67 | owner.addEventListener("click", (event, _id=owner_info.id) => { 68 | if(event.target.className == "follow") return; 69 | render.owner(_id); 70 | }); 71 | let thumbnail = document.createElement("img"); 72 | thumbnail.draggable = false; 73 | thumbnail.loading = "lazy"; 74 | thumbnail.className = "thumbnail"; 75 | thumbnail.src = owner_info?.thumbnails?.[0]?.url; 76 | owner.append(thumbnail); 77 | let name = document.createElement("span"); 78 | name.className = "name"; 79 | name.innerText = owner_info?.name ?? ""; 80 | name.setAttribute("verified", owner_info?.verified ?? false); 81 | owner.append(name); 82 | let followers = document.createElement("span"); 83 | followers.className = "followers"; 84 | followers.innerText = `${(owner_info?.followers ?? 0)} ${i18n.text("followers")}`; 85 | owner.append(followers); 86 | let follow = document.createElement("material-symbol"); 87 | follow.className = "follow"; 88 | follow.innerText = ["notifications", "notifications_active"][Number(database.following.is_following(owner_info.id))]; 89 | follow.addEventListener("click", async () => { 90 | if(!database.following.is_following(owner_info.id)){ // If not following yet 91 | follow.innerText = "notifications_active"; 92 | database.following.add({ 93 | id: owner_info.id, 94 | name: owner_info.name, 95 | followers: owner_info.followers, 96 | verified: owner_info.verified, 97 | thumbnail: await image_helper.data_uri.from_image_uri(owner_info?.thumbnails?.[0]?.url), 98 | }); 99 | } else { 100 | follow.innerText = "notifications"; 101 | database.following.remove(owner_info.id); 102 | }; 103 | }); 104 | owner.append(follow); 105 | return owner; 106 | }, 107 | chips: (parent, keywords, event_handler) => { 108 | parent.innerHTML = ""; 109 | keywords.forEach(keyword => { 110 | let chip = document.createElement("div"); 111 | chip.innerText = keyword; 112 | chip.addEventListener("click", (_, _event_handler=event_handler) => { 113 | _event_handler(keyword); 114 | }); 115 | parent.append(chip); 116 | }); 117 | }, 118 | video_player_details: (parent, list, title) => { 119 | parent.innerHTML = ""; 120 | if(title != null){ 121 | let title_element = document.createElement("div"); 122 | title_element.className = "title"; 123 | title_element.innerText = title; 124 | parent.append(title_element); 125 | } 126 | list.forEach((text, index) => { 127 | let list_item = document.createElement("div"); 128 | list_item.className = "list-item"; 129 | if(typeof(text) == "string") { 130 | list_item.setAttribute("value", index); 131 | list_item.innerText = text; 132 | } else { 133 | list_item.setAttribute("value", text.value); 134 | list_item.innerText = text.text; 135 | } 136 | parent.append(list_item); 137 | }); 138 | }, 139 | info_with_kaomoji: (_info, _kaomoji) => { 140 | let container = document.createElement("div"); 141 | container.className = "info_with_kaomoji"; 142 | let kaomoji = document.createElement("span"); 143 | kaomoji.className = "kaomoji"; 144 | kaomoji.innerText = i18n.text(_kaomoji); 145 | container.append(kaomoji); 146 | container.append(document.createElement("br")); 147 | container.append(i18n.text(_info)); 148 | return container; 149 | }, 150 | queue: (element) => { 151 | element.innerHTML = ""; 152 | let active_track = components.tabs.watch.video.$_queue_active(); 153 | components.tabs.watch.video.$queue.forEach((video_preview, index) => { 154 | let preview = render.$.video_preview("list", {...video_preview, keep_queue:true}); 155 | if(active_track == index){ 156 | preview.className += " active"; 157 | }; 158 | element.append(preview); 159 | }); 160 | }, 161 | thumbar_buttons: (is_playing) => { 162 | components.$.$_window.setThumbarButtons([ 163 | { 164 | tooltip: "Previous", 165 | icon: electron.nativeImage.createFromPath(path.join(__dirname, "assets/material-symbols/skip_previous.png")), 166 | click: () => { 167 | components.tabs.watch.video.$_previous(); 168 | } 169 | }, { 170 | tooltip: (is_playing) ? "Pause" : "Play", 171 | icon: electron.nativeImage.createFromPath(path.join(__dirname, "assets/material-symbols/" + ((is_playing) ? "pause" : "play_arrow") + ".png")), 172 | click: () => { 173 | components.tabs.watch.video.$_play_pause(); 174 | } 175 | }, { 176 | tooltip: "Next", 177 | icon: electron.nativeImage.createFromPath(path.join(__dirname, "assets/material-symbols/skip_next.png")), 178 | click: () => { 179 | components.tabs.watch.video.$_next(); 180 | } 181 | } 182 | ]); 183 | }, 184 | comment_preview: (info) => { 185 | let __channel_opener = (event, channel_id=info.owner.id) => { 186 | render.owner(channel_id); 187 | }; 188 | let outer = document.createElement("div"); 189 | outer.className = "-comment-preview"; 190 | let thumbnail = document.createElement("img"); 191 | thumbnail.draggable = false; 192 | thumbnail.src = info.owner.thumbnails?.[0]?.url; 193 | thumbnail.addEventListener("click", __channel_opener); 194 | outer.append(thumbnail); 195 | let details = document.createElement("div"); 196 | outer.append(details); 197 | let name = document.createElement("div"); 198 | name.className = "name"; 199 | name.innerText = info.owner.name; 200 | name.setAttribute("verified", info.owner.isVerified); 201 | name.addEventListener("click", __channel_opener); 202 | details.append(name); 203 | let text = document.createElement("div"); 204 | text.className = "text"; 205 | text.innerText = info.text; 206 | details.append(text); 207 | return outer; 208 | } 209 | }, 210 | trends: async () => { 211 | components.tabs.trends.$.innerHTML = ""; 212 | 213 | let trends = await yt_extractor.trends.get_trends(); 214 | trends.videos.forEach(video_preview => { 215 | components.tabs.trends.$.append(render.$.video_preview("compact", { 216 | id: video_preview.id, 217 | title: video_preview.title, 218 | thumbnail: video_preview.thumbnails[0].url, 219 | owner: { 220 | name: video_preview.owner.name, 221 | verified: video_preview.owner.verified 222 | }, 223 | length: video_preview.length 224 | })); 225 | }); 226 | }, 227 | watch: async (id) => { 228 | components.tabs.watch.$.scrollBy(0, -99999); 229 | let previous_title = components.tabs.watch.info.title.innerText; 230 | components.tabs.watch.info.title.innerText = i18n.text("loading"); 231 | 232 | if(!yt_extractor.video.is_extracted){ 233 | await yt_extractor.video.extract_youtube_algorithm(); 234 | } 235 | 236 | let response = await yt_extractor.video.get_video(id); 237 | response.id = id; 238 | components.tabs.watch.$$response = response; 239 | 240 | if(!response.isFamilySafe) { 241 | if(!confirm(i18n.text("warning_not_family_safe_video"))){ 242 | components.tabs.watch.info.title.innerText = previous_title; 243 | return; 244 | }; 245 | }; 246 | 247 | if(components.tabs.watch.video.$hls != null){ 248 | components.tabs.watch.video.$hls.destroy(); 249 | components.tabs.watch.video.$hls = null; 250 | }; 251 | 252 | components.tabs.watch.video.$_unload(); 253 | 254 | components.tabs.watch.video.$video_tracks = []; 255 | components.tabs.watch.video.$audio_tracks = []; 256 | components.tabs.watch.video.$related_tracks = []; 257 | if(!components.tabs.watch.$$response.playability.isLiveNow){ 258 | if(components.tabs.watch.$$response.playability.isLive && components.tabs.watch.$$response.playability.streamTime) { 259 | components.tabs.watch.video.video_player.stream_date.$.setAttribute("show", true); 260 | components.tabs.watch.video.video_player.stream_date.date.innerText = components.tabs.watch.$$response.playability.streamTime.toLocaleString(); 261 | } else { 262 | components.tabs.watch.video.video_player.stream_date.$.setAttribute("show", false); 263 | ([ 264 | [response.videoStreams, components.tabs.watch.video.$video_tracks], 265 | [response.audioStreams, components.tabs.watch.video.$audio_tracks], 266 | [response.relatedStreams, components.tabs.watch.video.$related_tracks] 267 | ]).map(target => { 268 | target[0].map(stream => { 269 | target[1].push(stream); 270 | }); 271 | }); 272 | 273 | components.tabs.watch.video.$_change_stream("video", -1); 274 | components.tabs.watch.video.$_change_stream("audio", -1); 275 | }; 276 | } else { 277 | // Since Newflow pip is not compatible with live streams 278 | // Close Newflow pip until live stream ends. 279 | if(components.tabs.watch.video.$pip.$is_pip){ 280 | components.tabs.watch.video.$pip.$toggle(); 281 | } 282 | components.tabs.watch.video.$hls = new Hls(); 283 | components.tabs.watch.video.$hls.loadSource(components.tabs.watch.$$response.hls); 284 | components.tabs.watch.video.$hls.attachMedia(components.tabs.watch.video.$video); 285 | components.tabs.watch.video.$hls.on(Hls.Events.MANIFEST_PARSED,function() { 286 | components.tabs.watch.video.$video_tracks = components.tabs.watch.video.$hls.levels.map(a=>a.height+"p"); 287 | render.$.video_player_details( 288 | components.tabs.watch.video.video_player.controls.details.$$.video_quality, 289 | components.tabs.watch.video.$video_tracks, 290 | i18n.text("video_quality") 291 | ); 292 | video.play(); 293 | }); 294 | } 295 | 296 | components.playbar.controls.play_pause.innerText = "play_arrow"; 297 | components.tabs.watch.video.video_player.controls.play_pause.innerText = "play_arrow"; 298 | 299 | components.tabs.watch.info.title.innerText = response.title; 300 | components.playbar.title.innerText = response.title; 301 | components.tabs.watch.info.description.innerText = response.description; 302 | components.tabs.watch.video.video_player.$.setAttribute("timelines", components.tabs.watch.video.$_times(response.description)); 303 | components.tabs.watch.info.controls.like.setAttribute("count", response.likes); 304 | render.$.chips(components.tabs.watch.info.keywords, response.keywords, render.search); 305 | components.tabs.watch.info.owner.thumbnail.src = response.owner.thumbnails[0].url; 306 | components.playbar.thumbnail.src = response.thumbnails[0].url; 307 | components.tabs.watch.video.video_player.thumbnail.src = response.thumbnails[0].url; 308 | components.tabs.watch.video.$video.poster = response.thumbnails[0].url; 309 | components.tabs.watch.info.owner.name.innerText = response.owner.name; 310 | components.tabs.watch.info.owner.name.setAttribute("verified", response.owner.verified); 311 | components.playbar.owner.innerText = response.owner.name; 312 | components.tabs.watch.info.owner.followers.innerText = `${response.owner.followers} ${i18n.text("followers")}`; 313 | 314 | components.tabs.watch.video.$_add_track({ 315 | id: id, 316 | title: response.title, 317 | thumbnail: response.thumbnails[0].url, 318 | length: response.length, 319 | owner: { 320 | name: response.owner.name, 321 | verified: response.owner.verified 322 | } 323 | }, true); 324 | if(components.tabs.watch.panels.autoplay.checked 325 | && components.tabs.watch.video.$queue[components.tabs.watch.video.$_queue_active()] == components.tabs.watch.video.$queue.at(-1)){ // if the playing track is last track 326 | for(let next_index = 0; next_index < response.nextVideos.length; next_index++) { 327 | let video_preview = response.nextVideos[next_index]; 328 | if(video_preview.length > 15*60) { // TODO: @@@ some settings If video longer than 15min 329 | continue; 330 | } 331 | video_preview.thumbnail = video_preview.thumbnails?.[next_index]?.url ?? null; 332 | if(components.tabs.watch.video.$_add_track(video_preview, false)){ 333 | break; 334 | } 335 | } 336 | }; 337 | 338 | if(components.tabs.watch.video.$times != []){ 339 | render.$.video_player_details( 340 | components.tabs.watch.video.video_player.controls.details.$$.timelines, 341 | components.tabs.watch.video.$times.map(timeline => ({ 342 | text: timeline[1], 343 | value: timeline[0] 344 | })), 345 | i18n.text("timelines") 346 | ); 347 | }; 348 | 349 | components.tabs.watch.video.video_player.controls.previous.style.setProperty("display", ["none","unset"][Number(components.tabs.watch.video.$_can_previous())]); 350 | components.tabs.watch.video.video_player.controls.next.style.setProperty("display", ["none","unset"][Number(components.tabs.watch.video.$_can_next())]); 351 | 352 | render.$.video_player_details( 353 | components.tabs.watch.video.video_player.controls.details.$$.audio_quality, 354 | components.tabs.watch.video.$audio_tracks.map(track => { 355 | let codec = track.mimeType; 356 | if(track.mimeType.includes("mp4a")){ 357 | codec = "m4a"; 358 | } else if(track.mimeType.includes("opus")) { 359 | codec = "opus"; 360 | }; 361 | let language = ""; 362 | if(track?.audioTrack){ 363 | language = `${track?.audioTrack?.displayName} / `; 364 | } 365 | let bitrate = Math.round(track.bitrate / 1024); 366 | return `${language}${bitrate} kbps / ${codec}`; 367 | }), 368 | i18n.text("audio_quality") 369 | ); 370 | 371 | render.$.video_player_details( 372 | components.tabs.watch.video.video_player.controls.details.$$.video_quality, 373 | components.tabs.watch.video.$video_tracks.map(track => { 374 | let codec = track.mimeType; 375 | if(track.mimeType.includes("mp4")){ 376 | codec = "mpeg-4"; 377 | } else if(track.mimeType.includes("webm")) { 378 | codec = "webm"; 379 | }; 380 | return `${track.qualityLabel} / ${codec}`; 381 | }), 382 | i18n.text("video_quality") 383 | ); 384 | 385 | components.tabs.watch.video.$captions.$_remove(); 386 | render.$.video_player_details( 387 | components.tabs.watch.video.video_player.controls.details.$$.captions, 388 | [ 389 | {text:i18n.text("disable"), value:-1}, 390 | ...response.captions.map( (track,index) => ({text:track?.name?.simpleText ?? track?.name?.runs?.[0]?.text, value:index})) 391 | ], 392 | i18n.text("captions") 393 | ); 394 | 395 | components.tabs.watch.next_videos.innerHTML = ""; 396 | response.nextVideos.forEach(video_preview => { 397 | components.tabs.watch.next_videos.append(render.$.video_preview("compact", { 398 | id: video_preview.id, 399 | title: video_preview.title, 400 | thumbnail: video_preview.thumbnails[0].url, 401 | owner: { 402 | name: video_preview.owner.name, 403 | verified: video_preview.owner.verified 404 | }, 405 | length: video_preview.length 406 | })); 407 | }); 408 | 409 | // TODO: Auto start playing video 410 | components.tabs.watch.video.$video.addEventListener("canplay", () => { 411 | components.tabs.watch.video.$_play(); 412 | }, {once: true}); 413 | 414 | (async () => { 415 | // TODO: @@@watch.mediaSession.square_artwork(false||false|true) 416 | /*let the_artwork = [{ 417 | src: response.thumbnails[0].url, 418 | sizes: `${response.thumbnails[0].width}x${response.thumbnails[0].height}` 419 | }];*/ 420 | 421 | let the_artwork = [{ 422 | src: await image_helper.crop.as_square(response.thumbnails[0].url, 256, response.thumbnails[0].width, response.thumbnails[0].height), 423 | sizes: `256x256` 424 | }]; 425 | 426 | navigator.mediaSession.metadata = new MediaMetadata({ 427 | title: response.title, 428 | artist: response.owner.name, 429 | artwork: the_artwork 430 | }); 431 | })(); 432 | 433 | (async () => { 434 | // TODO: @@@ui.styles.video_theme_colors(true||false|true) 435 | let dominant_color = await image_helper.dominant_color(response.thumbnails[0].url); 436 | document.body.setAttribute("theme-variant", "player-effects"); 437 | document.body.style.setProperty("--effect-color", dominant_color); 438 | })(); 439 | 440 | // Add/Update history record 441 | (async () => { 442 | database.history.add({ 443 | title: response.title, 444 | id: id, 445 | owner_name: response.owner.name, 446 | thumbnail: await image_helper.data_uri.from_image_uri(response.thumbnails[0].url), 447 | length: response.length 448 | }); 449 | })(); 450 | 451 | (async () => { 452 | if(database.following.is_following(response.owner.id)){ 453 | components.tabs.watch.info.owner.follow.innerText = "notifications_active"; 454 | } else { 455 | components.tabs.watch.info.owner.follow.innerText = "notifications"; 456 | } 457 | })(); 458 | 459 | // Update playing queue 460 | render.$.queue(components.tabs.watch.panels.playing_queue); 461 | render.$.queue(components.tabs.queue.$); 462 | 463 | components.tabs.watch.comments.innerHTML = ""; 464 | let comments = await yt_extractor.video.get_comments(response.commentsToken); 465 | comments?.comments?.forEach(data=>components.tabs.watch.comments.append(render.$.comment_preview(data))); 466 | }, 467 | owner: async (id) => { 468 | let data = await yt_extractor.owner.get_owner(id); 469 | components.tabs.owner.$$response = data; 470 | components.tabs.$_switch("owner"); 471 | if(data.backgrounds?.[0]?.url != null){ 472 | components.tabs.owner.banner.background.setAttribute("banner", true); 473 | components.tabs.owner.banner.background.src = data.backgrounds?.[0]?.url; 474 | } else { 475 | components.tabs.owner.banner.background.setAttribute("banner", false); 476 | } 477 | components.tabs.owner.banner.description.innerText = data.description; 478 | components.tabs.owner.banner.followers.innerText = data.followers + " Followers"; 479 | components.tabs.owner.banner.name.innerText = data.name; 480 | components.tabs.owner.banner.video_count.innerText = data.videosCount + " Videos"; 481 | components.tabs.owner.banner.thumbnail.src = data.thumbnails?.[0]?.url; 482 | components.tabs.owner.banner.name.setAttribute("verified", data.verified); 483 | 484 | components.tabs.owner.videos.innerHTML = ""; 485 | 486 | if(data.videos != null) { 487 | data.videos.forEach(video_preview => { 488 | components.tabs.owner.videos.append(render.$.video_preview("compact", { 489 | id: video_preview.id, 490 | title: video_preview.title, 491 | thumbnail: video_preview.thumbnails?.[0]?.url ?? null, 492 | length: video_preview.length, 493 | owner: { 494 | name: data.name 495 | } 496 | })); 497 | }); 498 | }; 499 | 500 | (async () => { 501 | if(database.following.is_following(data.id)){ 502 | components.tabs.owner.banner.follow.innerText = "notifications_active"; 503 | } else { 504 | components.tabs.owner.banner.follow.innerText = "notifications"; 505 | } 506 | })(); 507 | }, 508 | search: async (query) => { 509 | components.tabs.$_switch("search"); 510 | components.titlebar.search.value = query; 511 | components.tabs.search.$.innerHTML = ""; 512 | let response = await yt_extractor.search.search(query); 513 | response.forEach(video_preview => { 514 | components.tabs.search.$.append(render.$.video_preview("normal", { 515 | id: video_preview.id, 516 | title: video_preview.title, 517 | thumbnail: video_preview.thumbnails[0].url, 518 | owner: { 519 | name: video_preview.owner.name, 520 | verified: video_preview.owner.verified 521 | }, 522 | length: video_preview.length 523 | })); 524 | }); 525 | }, 526 | history: async () => { 527 | components.tabs.history.$.innerHTML = ""; 528 | if(database.history.content.length == 0) return components.tabs.history.$.append(render.$.info_with_kaomoji("there_are_nothing_more_dog", "kaomoji_dog")); 529 | database.history.content.toReversed().forEach(video_preview => { 530 | components.tabs.history.$.append(render.$.video_preview("normal", { 531 | id: video_preview.id, 532 | title: video_preview.title, 533 | thumbnail: video_preview.thumbnail, 534 | owner: { 535 | name: video_preview.owner_name, 536 | verified: false 537 | }, 538 | length: video_preview.length 539 | })); 540 | }); 541 | }, 542 | following: async () => { 543 | components.tabs.following.$.innerHTML = ""; 544 | if(database.following.content.length == 0) return components.tabs.following.$.append(render.$.info_with_kaomoji("there_are_nothing_more_dog", "kaomoji_dog")); 545 | database.following.content.forEach(owner => { 546 | owner.thumbnails = [{url: owner.thumbnail}]; 547 | components.tabs.following.$.append(render.$.owner_preview("compact", owner)); 548 | }); 549 | }, 550 | feed: async () => { 551 | components.tabs.feed.$.innerHTML = ""; 552 | if((database.feed.content?.feed?.length ?? 0) == 0) return components.tabs.feed.$.append(render.$.info_with_kaomoji("there_are_nothing_more_dog", "kaomoji_dog")); 553 | database.feed.content.feed.forEach(video_preview => { 554 | components.tabs.feed.$.append(render.$.video_preview("normal", { 555 | id: video_preview.id, 556 | title: video_preview.title, 557 | thumbnail: video_preview.thumbnail.url, 558 | owner: { 559 | name: video_preview.owner.name, 560 | verified: video_preview.owner.verified 561 | }, 562 | length: video_preview.length 563 | })); 564 | }); 565 | }, 566 | downloads: async () => { 567 | components.tabs.downloads.$.innerHTML = ""; 568 | if(components.tabs.downloads.$list.length == 0) return components.tabs.downloads.$.append(render.$.info_with_kaomoji("there_are_nothing_more_dog", "kaomoji_dog")); 569 | components.tabs.downloads.$list.forEach((media, index) => { 570 | let renderer = render.$.video_preview("normal", { 571 | title: media.properties.title, 572 | owner: { 573 | name: media.properties.owner_name 574 | }, 575 | length: media.properties.length, 576 | id: media.properties.id, 577 | thumbnail: media.properties.thumbnail 578 | }); 579 | renderer.style.setProperty("--progress", (media.contentSize/media.contentLength*100)+"%"); 580 | renderer.setAttribute("state", media.state); 581 | components.tabs.downloads.$.append(renderer); 582 | }); 583 | }, 584 | playlists: async () => { 585 | components.tabs.playlists.$.innerText = ""; 586 | Object.keys(database.playlists.content).forEach(playlist => { 587 | let playlist_data = database.playlists.content[playlist]; 588 | components.tabs.playlists.$.append(render.$.playlist_preview("normal", { 589 | id: playlist, 590 | title: playlist_data.title, 591 | thumbnail: playlist_data.thumbnail 592 | })); 593 | }); 594 | } 595 | }; 596 | -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | @import url(./background-material.css); 2 | 3 | *, *:before, *:after { /* Disable forcing high contrast theme due to causing unreadable text and low contrast */ 4 | forced-color-adjust: none !important; 5 | } 6 | 7 | body { 8 | --border-thick: 2px; 9 | --border-radius-size: 5px; 10 | --transition-duration: 100ms; 11 | --system-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 12 | --effect-color: #777777; 13 | 14 | &[theme="dark"] { 15 | --theme-seconder-color: color-mix(in srgb, var(--effect-color) 60%, #000000); 16 | --theme-seconder-background-color: #222222; 17 | --theme-other-color: #444444; 18 | --theme-background-color: #111111; 19 | --theme-border-color: #777777; 20 | --theme-hover-color: #555555; 21 | --theme-text-color: #ffffff; 22 | --effect-color-percent: 30%; 23 | 24 | & * { 25 | color-scheme: dark; 26 | } 27 | } 28 | &[theme="black"] { 29 | --theme-seconder-color: color-mix(in srgb, var(--effect-color) 60%, #000000); 30 | --theme-seconder-background-color: #111111; 31 | --theme-other-color: #333333; 32 | --theme-background-color: #000000; 33 | --theme-border-color: #333333; 34 | --theme-hover-color: #444444; 35 | --theme-text-color: #ffffff; 36 | --effect-color-percent: 15%; 37 | 38 | 39 | & * { 40 | color-scheme: dark; 41 | } 42 | } 43 | &[theme="light"] { 44 | --theme-seconder-color: color-mix(in srgb, var(--effect-color) 60%, #ffffff); 45 | --theme-seconder-background-color: #cccccc; 46 | --theme-other-color: #aaaaaa; 47 | --theme-background-color: #dddddd; 48 | --theme-border-color: #888888; 49 | --theme-hover-color: #999999; 50 | --theme-text-color: #000000; 51 | --effect-color-percent: 50%; 52 | 53 | & * { 54 | color-scheme: light; 55 | } 56 | } 57 | &:not([theme-variant]){ 58 | --seconder-color: var(--theme-seconder-color); 59 | --seconder-background-color: var(--theme-seconder-background-color); 60 | --other-color: var(--theme-other-color); 61 | --background-color: var(--theme-background-color); 62 | --border-color: var(--theme-border-color); 63 | --hover-color: var(--theme-hover-color); 64 | --text-color: var(--theme-text-color); 65 | } 66 | &[theme-variant="player-effects"] { 67 | --seconder-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-seconder-color)); 68 | --seconder-background-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-seconder-background-color)); 69 | --other-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-other-color)); 70 | --background-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-background-color)); 71 | --border-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-border-color)); 72 | --hover-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-hover-color)); 73 | --text-color: color-mix(in srgb, var(--effect-color) var(--effect-color-percent), var(--theme-text-color)); 74 | } 75 | } 76 | 77 | body { 78 | background: var(--background-color); 79 | } 80 | 81 | * { 82 | color: var(--text-color); 83 | font-family: var(--system-font); 84 | /* font-weight: 800; TODO: @@@application.ui.fonts.use-bold-variant */ 85 | } 86 | 87 | /* Scrollbar theme */ 88 | 89 | ::-webkit-scrollbar, ::-webkit-scrollbar-button:single-button { 90 | width: 16px; 91 | height: 16px; 92 | } 93 | 94 | ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { 95 | background: transparent; 96 | } 97 | 98 | ::-webkit-scrollbar-thumb { 99 | min-height: 50px; 100 | min-width: 50px; 101 | background: var(--other-color); 102 | border-radius: var(--border-radius-size); 103 | } 104 | 105 | ::-webkit-scrollbar-button:single-button { 106 | background: var(--other-color); 107 | border-radius: var(--border-radius-size); 108 | background-size: 16px; 109 | } 110 | 111 | ::-webkit-scrollbar-button:single-button:hover { 112 | background: var(--hover-color); 113 | background-size: 16px; 114 | } 115 | 116 | ::-webkit-scrollbar-thumb:hover { 117 | background: var(--hover-color); 118 | } 119 | 120 | ::-webkit-scrollbar-button:single-button:vertical:decrement, ::-webkit-scrollbar-button:single-button:vertical:decrement:hover { 121 | background-image: url("../assets/material-symbols/keyboard_arrow_up.png"); 122 | } 123 | 124 | ::-webkit-scrollbar-button:single-button:vertical:increment, ::-webkit-scrollbar-button:single-button:vertical:increment:hover { 125 | background-image: url("../assets/material-symbols/keyboard_arrow_down.png"); 126 | } 127 | 128 | ::-webkit-scrollbar-button:single-button:horizontal:decrement, ::-webkit-scrollbar-button:single-button:horizontal:decrement:hover { 129 | background-image: url("../assets/material-symbols/keyboard_arrow_left.png"); 130 | } 131 | 132 | ::-webkit-scrollbar-button:single-button:horizontal:increment, ::-webkit-scrollbar-button:single-button:horizontal:increment:hover { 133 | background-image: url("../assets/material-symbols/keyboard_arrow_right.png"); 134 | } 135 | 136 | /* Input range theme*/ 137 | input[type="range"] { 138 | height: 12px; 139 | appearance: none; 140 | background: transparent; 141 | border-radius: 5px; 142 | clip-path: inset(0 round 10px); 143 | } 144 | 145 | input[type="range"]::-webkit-slider-runnable-track { 146 | width: 100%; 147 | background: var(--theme-background-color); 148 | height: 12px; 149 | border-radius: 5px; 150 | } 151 | 152 | input[type="range"]::-webkit-slider-thumb { 153 | appearance: none; 154 | height: 12px; 155 | width: 12px; 156 | background: var(--hover-color); 157 | box-shadow: -100vw 0 0 calc(100vw - 8px) var(--background-color); 158 | } 159 | 160 | .titlebar { 161 | & .button-layout[custom] { 162 | & button { 163 | border-radius: 0; 164 | 165 | &[style*="order"][style*="0"] { 166 | border-radius: 0 0 0 var(--border-radius-size); 167 | } 168 | 169 | &:not([show]) { 170 | display: none; 171 | } 172 | } 173 | } 174 | 175 | & button { 176 | border: none; 177 | background: transparent; 178 | transition-duration: var(--transition-duration); 179 | 180 | &:hover { 181 | background: var(--hover-color); 182 | } 183 | 184 | &.menu { 185 | border-left: var(--border-color) var(--border-thick) solid; 186 | border-radius: 0 0 var(--border-radius-size) 0; 187 | } 188 | 189 | &.minimize { 190 | border-radius: 0 0 0 var(--border-radius-size); 191 | } 192 | } 193 | 194 | & .search { 195 | background: transparent; 196 | border: var(--border-color) var(--border-thick) solid; 197 | border-radius: var(--border-radius-size); 198 | } 199 | } 200 | 201 | .sidenav { 202 | transition-duration: var(--transition-duration); 203 | overflow-y: auto; 204 | overflow-x: hidden; 205 | 206 | & material-symbol { 207 | display: flex; 208 | align-items: center; 209 | justify-content: center; 210 | } 211 | 212 | & > * { 213 | border-left: var(--border-color) var(--border-thick) solid; 214 | border-radius: 0 var(--border-radius-size) var(--border-radius-size) 0; 215 | transition-duration: var(--transition-duration); 216 | 217 | &:hover { 218 | background: var(--hover-color); 219 | } 220 | } 221 | 222 | &[open] { 223 | border-right: var(--border-color) var(--border-thick) solid; 224 | border-radius: var(--border-radius-size); 225 | 226 | & .bottom-border { 227 | border-bottom: var(--border-color) var(--border-thick) solid; 228 | } 229 | } 230 | } 231 | 232 | div.playbar { 233 | background-color: var(--seconder-background-color); 234 | border-radius: var(--border-radius-size); 235 | 236 | & .controls button { 237 | aspect-ratio: 1; 238 | height: 32px; 239 | background: var(--other-color); 240 | border: none; 241 | transition-duration: var(--transition-duration); 242 | 243 | &:first-child { 244 | border-radius: var(--border-radius-size) 0 0 var(--border-radius-size); 245 | } 246 | 247 | &:not(:last-child) { 248 | border-right: var(--border-thick) var(--border-color) solid; 249 | } 250 | 251 | &:last-child { 252 | border-radius: 0 var(--border-radius-size) var(--border-radius-size) 0; 253 | } 254 | 255 | &:hover { 256 | background: var(--hover-color); 257 | } 258 | } 259 | 260 | @media (max-width: 600px) { 261 | border-radius: var(--border-radius-size) var(--border-radius-size) 0 0; 262 | } 263 | } 264 | 265 | body:has(div.tabs[active="trends"]) .sidenav:not([open]) div[tab="trends"], 266 | body:has(div.tabs[active="feed"]) .sidenav:not([open]) div[tab="feed"], 267 | body:has(div.tabs[active="following"]) .sidenav:not([open]) div[tab="following"], 268 | body:has(div.tabs[active="bookmarks"]) .sidenav:not([open]) div[tab="bookmarks"] { 269 | border-left: 2px solid var(--effect-color); 270 | } 271 | 272 | #watch { 273 | display: flex; 274 | gap: 10px; 275 | 276 | @media (max-width: 700px) { 277 | flex-direction: column; 278 | } 279 | &[mode="theatre"] { 280 | flex-direction: column; 281 | 282 | & .primary-flow .video-player { 283 | max-height: calc(100vh - calc(var(--titlebar-height) + 50px)); 284 | } 285 | 286 | & .seconder-flow { 287 | max-width: unset; 288 | min-width: unset; 289 | 290 | & .next-videos { 291 | flex-direction: row; 292 | flex-wrap: wrap; 293 | } 294 | } 295 | } 296 | 297 | & .primary-flow { 298 | flex-grow: 1; 299 | 300 | & .video-player { 301 | max-height: calc(100vh - calc(var(--titlebar-height) + 120px)); 302 | display: grid; 303 | grid-template-columns: 1fr minmax(min-content, 200px); 304 | grid-template-rows: minmax(0, 1fr) 0 minmax(0, 200px) auto auto; 305 | grid-template-areas: 306 | "video empty1" 307 | "video details" 308 | "stream-info details" 309 | "time time" 310 | "controls controls"; 311 | border-radius: var(--border-radius-size); 312 | 313 | &[timelines="false"] .timelines { 314 | display: none; 315 | } 316 | 317 | &:has(video[loading="true"]) .loading-circle { 318 | content: ""; 319 | pointer-events: none !important; 320 | 321 | opacity: 0.75 !important; 322 | 323 | position: relative; 324 | height: 65px; 325 | width: 65px; 326 | left: 50%; 327 | top: 50%; 328 | 329 | grid-row-start: video-start; 330 | grid-column-start: video-start; 331 | grid-row-end: controls-end; 332 | grid-column-end: controls-end; 333 | 334 | animation: loading-circle cubic-bezier(0.4, 0.25, 0.6, 0.75) 1.5s infinite; 335 | animation-delay: 0ms; 336 | 337 | border-radius: 50%; 338 | border-top-color: var(--effect-color) !important; 339 | border-color: var(--background-color); 340 | border-style: solid; 341 | border-width: 8px; 342 | } 343 | 344 | & .details { 345 | grid-area: details; 346 | background: var(--background-color); 347 | margin-right: 10px; 348 | margin-bottom: 5px; 349 | border-radius: var(--border-radius-size); 350 | overflow-x: hidden; 351 | overflow-y: auto; 352 | padding: 10px; 353 | height: min-content; 354 | max-height: 100%; 355 | align-self: end; 356 | 357 | 358 | & > * { 359 | margin-bottom: 2px; 360 | } 361 | 362 | & .title { 363 | font-size: 18px; 364 | text-decoration: underline; 365 | position: sticky; 366 | top: 0; 367 | background: inherit; 368 | margin-bottom: 10px; 369 | z-index: 1; 370 | 371 | &::after { 372 | content: "."; 373 | position: absolute; 374 | top: -10px; 375 | height: 38px; 376 | left: 0; 377 | width: 100%; 378 | background: inherit; 379 | color: #0000; 380 | z-index: -1; 381 | } 382 | } 383 | 384 | & :not(.title, material-symbol) { 385 | font-size: 14px; 386 | } 387 | 388 | & material-symbol { 389 | pointer-events: none; 390 | } 391 | 392 | &.list { 393 | & .list-item { 394 | padding: 5px 2px; 395 | border-radius: var(--border-radius-size); 396 | transition-duration: var(--transition-duration); 397 | display: flex; 398 | align-items: center; 399 | gap: 4px; 400 | 401 | &:hover { 402 | background: var(--hover-color); 403 | } 404 | } 405 | } 406 | 407 | &.filter { 408 | & > div { 409 | display: flex; 410 | flex-direction: column; 411 | gap: 4px; 412 | background: inherit; 413 | 414 | & div { 415 | display: flex; 416 | flex-direction: column; 417 | gap: 4px; 418 | } 419 | 420 | & button { 421 | margin-top: 8px; 422 | min-height: 24px; 423 | background: var(--other-color); 424 | border: none; 425 | border-radius: var(--border-radius-size); 426 | } 427 | } 428 | } 429 | } 430 | 431 | & video { /* Video Filters*/ 432 | clip-path: border-box; 433 | --filter-brightness: 1; /* 0 to 20 */ 434 | --filter-contrast: 1; /* 0 to 20 */ 435 | --filter-blur: 0px; /* 0px to 20px */ 436 | --filter-grayscale: 0; /* 0 to 1 */ 437 | --filter-invert: 0; /* 0 to 1 */ 438 | --filter-hue-rotate: 0deg; /* 0deg to 359deg*/ 439 | filter: brightness(var(--filter-brightness)) 440 | contrast(var(--filter-contrast)) 441 | blur(var(--filter-blur)) 442 | grayscale(var(--filter-grayscale)) 443 | invert(var(--filter-invert)) 444 | hue-rotate(var(--filter-hue-rotate)); 445 | } 446 | 447 | & video, .thumbnail { 448 | grid-column: video-start / controls-end; 449 | grid-row: video / controls; 450 | width: 100%; 451 | min-height: calc(100%); 452 | max-height: calc(100%); 453 | object-fit: contain; 454 | background: #000000; 455 | border-radius: var(--border-radius-size); 456 | } 457 | 458 | & .thumbnail { 459 | z-index: -2; 460 | } 461 | 462 | &:has(video) .thumbnail { 463 | display: none; 464 | } 465 | 466 | & > *:not(video, .thumbnail) { 467 | z-index: 1; 468 | } 469 | 470 | /* Hide controls auto; Will replace the CSS*/ 471 | & > *:not(video, .thumbnail, .stream-start-date) { 472 | opacity: 0; 473 | pointer-events: none; 474 | transition-duration: 200ms; 475 | transition-delay: 100ms; 476 | } &:hover > * { 477 | opacity: unset; 478 | pointer-events: unset; 479 | transition-delay: 0ms; 480 | } 481 | 482 | 483 | & .time { 484 | grid-area: time; 485 | background: linear-gradient( 486 | 0deg, 487 | color-mix(in srgb, var(--background-color) 50%, transparent), 488 | transparent 489 | ); 490 | padding: 0 10px; 491 | border-radius: var(--border-radius-size) var(--border-radius-size) 0 0; 492 | 493 | & input { 494 | accent-color: var(--effect-color); 495 | width: 100%; 496 | } 497 | } 498 | 499 | & .controls { 500 | grid-area: controls; 501 | background: linear-gradient( 502 | 0deg, 503 | var(--background-color), 504 | color-mix(in srgb, var(--background-color) 50%, transparent) 505 | ); 506 | display: flex; 507 | justify-content: space-between; 508 | padding: 2px 10px 8px 10px; 509 | border-radius: 0 0 var(--border-radius-size) var(--border-radius-size); 510 | 511 | & > div { 512 | display: flex; 513 | align-items: center; 514 | gap: 4px; 515 | } 516 | 517 | & .time-info { 518 | padding: 0 2px; 519 | } 520 | 521 | .hide-range { 522 | display: flex; 523 | gap: 6px; 524 | align-items: center; 525 | 526 | &:not(:hover) input[type="range"] { 527 | opacity: 0; 528 | visibility: hidden; 529 | width: 0px; 530 | pointer-events: none; 531 | transition-delay: 200ms; 532 | } 533 | 534 | & input[type="range"] { 535 | transition-duration: var(--transition-duration); 536 | accent-color: var(--effect-color); 537 | width: 80px; 538 | } 539 | } 540 | } 541 | 542 | & .stream-start-date { 543 | display: none; 544 | 545 | &[show="true"]{ 546 | grid-area: stream-info; 547 | padding: 16px; 548 | margin-bottom: 4px; 549 | background: var(--theme-seconder-color); 550 | width: min-content; 551 | height: min-content; 552 | text-wrap: nowrap; 553 | border-radius: 0 var(--border-radius-size) var(--border-radius-size) 0; 554 | display: unset; 555 | align-self: flex-end; 556 | } 557 | } 558 | } 559 | 560 | & .info { 561 | display: grid; 562 | grid-template-columns: 1fr 1fr; 563 | grid-template-areas: 564 | "title title" 565 | "owner controls" 566 | "description description" 567 | "keywords keywords"; 568 | gap: 5px; 569 | 570 | @media (max-width: 1000px) { 571 | grid-template-columns: 1fr; 572 | grid-template-areas: 573 | "title" 574 | "owner" 575 | "controls" 576 | "description" 577 | "keywords"; 578 | 579 | } 580 | 581 | & .title { 582 | grid-area: title; 583 | font-size: 25px; 584 | padding: 4px; 585 | text-overflow: ellipsis; 586 | overflow: hidden; 587 | text-wrap: nowrap; 588 | } 589 | 590 | & .owner { 591 | width: min-content; 592 | 593 | @media (max-width: 1000px) { 594 | width: unset; 595 | } 596 | } 597 | 598 | & .controls { 599 | grid-area: controls; 600 | display: flex; 601 | justify-content: right; 602 | align-items: center; 603 | margin-right: 10px; 604 | 605 | @media (max-width: 1000px) { 606 | justify-content: center; 607 | margin: unset; 608 | } 609 | 610 | & button { 611 | aspect-ratio: 1; 612 | height: 32px; 613 | background: var(--other-color); 614 | border: none; 615 | transition-duration: var(--transition-duration); 616 | 617 | &.like { 618 | aspect-ratio: unset; 619 | width: auto; 620 | display: flex; 621 | align-items: center; 622 | gap: 5px; 623 | padding: 0 4px; 624 | &::after { 625 | content: attr(count); 626 | font-family: var(--system-font); 627 | font-size: 16px; 628 | } 629 | } 630 | 631 | &:first-child { 632 | border-radius: var(--border-radius-size) 0 0 var(--border-radius-size); 633 | } 634 | 635 | &:not(:last-child) { 636 | border-right: var(--border-thick) var(--border-color) solid; 637 | } 638 | 639 | &:last-child { 640 | border-radius: 0 var(--border-radius-size) var(--border-radius-size) 0; 641 | } 642 | 643 | &:hover { 644 | background: var(--hover-color); 645 | } 646 | } 647 | } 648 | 649 | & .description { 650 | grid-area: description; 651 | user-select: text; 652 | word-break: break-word; 653 | } 654 | 655 | & .keywords { 656 | grid-area: keywords; 657 | display: flex; 658 | flex-wrap: wrap; 659 | gap: 5px; 660 | 661 | & > * { 662 | background: var(--effect-color); 663 | padding: 5px; 664 | border-radius: 5px; 665 | cursor: pointer; 666 | display: flex; 667 | align-items: center; 668 | justify-content: center; 669 | max-height: 25px; 670 | } 671 | } 672 | } 673 | 674 | & .comments { 675 | display: flex; 676 | flex-direction: column; 677 | gap: 8px; 678 | padding: 16px 0; 679 | } 680 | } 681 | 682 | & .seconder-flow { 683 | min-width: 200px; 684 | max-width: 200px; 685 | @media (min-width: 1000px) { 686 | min-width: 250px; 687 | max-width: 250px; 688 | } 689 | @media (min-width: 1500px) { 690 | min-width: 325px; 691 | max-width: 325px; 692 | } 693 | 694 | & > * { 695 | padding: 5px; 696 | } 697 | 698 | &:not(:has(.next-videos > *)){ 699 | min-width: 0; 700 | max-width: 0; 701 | display: none; 702 | } 703 | 704 | & .panels { 705 | gap: 10px; 706 | display: flex; 707 | flex-direction: column; 708 | 709 | & .autoplay { 710 | display: flex; 711 | align-items: center; 712 | gap: 8px; 713 | } 714 | 715 | & > *:not(.autoplay) { 716 | display: flex; 717 | flex-direction: column; 718 | max-height: min(max(30vh, 300px), 65vh); 719 | overflow-y: auto; 720 | gap: 2px; 721 | padding-right: 4px; 722 | } 723 | } 724 | 725 | & .next-videos { 726 | gap: 10px; 727 | display: flex; 728 | flex-direction: column; 729 | align-items: center; 730 | justify-content: center; 731 | } 732 | } 733 | } 734 | 735 | dialog#share[open] { 736 | background: var(--other-color); 737 | border-radius: var(--border-radius-size); 738 | border: var(--border-thick) var(--border-color) solid; 739 | 740 | & .title { 741 | font-size: 24px; 742 | } 743 | } 744 | 745 | #search, #history, #feed, #downloads { 746 | display: flex; 747 | flex-direction: column; 748 | align-items: center; 749 | gap: 5px; 750 | 751 | & .-video-preview { 752 | width: 80%; 753 | 754 | @media (max-width: 450px){ 755 | width: 100%; 756 | } 757 | } 758 | } 759 | 760 | #downloads { 761 | .normal.-video-preview[state="downloading"] { 762 | --progress: 0%; 763 | background: linear-gradient(90deg, #aaaa22 var(--progress), transparent calc(var(--progress) + 25px)); 764 | } 765 | 766 | .normal.-video-preview[state="finish"] { 767 | background: #22aa22; 768 | } 769 | } 770 | 771 | #following { 772 | display: flex; 773 | flex-direction: column; 774 | align-items: center; 775 | gap: 5px; 776 | 777 | & .-owner-preview { 778 | width: 80%; 779 | 780 | @media (max-width: 450px){ 781 | width: 100%; 782 | } 783 | } 784 | } 785 | 786 | #trends, #owner .videos { 787 | display: flex; 788 | flex-direction: row; 789 | flex-wrap: wrap; 790 | gap: 5px; 791 | justify-content:center; 792 | } 793 | 794 | #settings { 795 | display: flex; 796 | flex-direction: column; 797 | gap: 5px; 798 | 799 | &:has(details[open]) > details:not([open]) { 800 | display:none; 801 | } 802 | 803 | & > details { 804 | background: var(--seconder-background-color); 805 | padding: 5px 15px; 806 | border-radius: var(--border-radius-size); 807 | transition-duration: 100ms; 808 | 809 | &:not([open]):hover { 810 | background: var(--hover-color); 811 | } 812 | 813 | & > summary { 814 | list-style-type: none; 815 | font-size: 24px; 816 | line-height: 24px; 817 | } 818 | 819 | & > ul { 820 | display: flex; 821 | flex-direction: column; 822 | gap: 5px; 823 | list-style: none; 824 | 825 | & > li { 826 | display: flex; 827 | justify-content: space-between; 828 | padding: 5px; 829 | transition-duration: 100ms; 830 | 831 | &:hover { 832 | background: var(--hover-color); 833 | border-radius: var(--border-radius-size); 834 | } 835 | } 836 | } 837 | 838 | &[open] > summary { 839 | margin-bottom: 10px; 840 | 841 | &::before { 842 | content: "arrow_back"; 843 | margin-right: 4px; 844 | font-family: "Material Symbols Rounded" 845 | } 846 | } 847 | } 848 | } 849 | 850 | [verified="true"]::after { 851 | content: "verified"; 852 | font-family: "Material Symbols Rounded"; 853 | color: var(--text-color); 854 | transform: translate(4px, 3px); 855 | display: inline-block; 856 | position: relative; 857 | line-height: 0; 858 | } 859 | 860 | video::cue { 861 | background: color-mix(in srgb, var(--effect-color) 75%, transparent); 862 | } 863 | 864 | ::selection { 865 | background: color-mix(in srgb, var(--effect-color) 75%, transparent); 866 | } 867 | 868 | .-video-preview, .-playlist-preview { 869 | display: grid; 870 | padding: 5px; 871 | border-radius: var(--border-radius-size); 872 | 873 | & img { 874 | border-radius: var(--border-radius-size); 875 | aspect-ratio: 16/9; 876 | object-fit: contain; 877 | background: #000000; 878 | } 879 | 880 | & .owner { 881 | grid-area: owner; 882 | } 883 | 884 | & .title { 885 | grid-area: title; 886 | } 887 | 888 | & .owner, .title { 889 | overflow: hidden; 890 | text-wrap: nowrap; 891 | text-overflow: ellipsis; 892 | } 893 | 894 | & .length { 895 | grid-area: time; 896 | margin: 0 5px 5px 0; 897 | padding: 4px; 898 | background: var(--hover-color); 899 | border-radius: var(--border-radius-size); 900 | 901 | width: max-content; 902 | min-width: min-content; 903 | max-width: 100%; 904 | place-self: flex-end; 905 | 906 | &[length="0"] { 907 | display: none; 908 | } 909 | } 910 | 911 | &.compact { 912 | width: 100%; 913 | max-width: 300px; 914 | grid-template-areas: 915 | "thumbnail thumbnail" 916 | "empty time" 917 | "title title" 918 | "owner owner"; 919 | gap: 2px; 920 | 921 | & img { 922 | width: 100%; 923 | grid-row-start: thumbnail; 924 | grid-row-end: time; 925 | grid-column-start: thumbnail; 926 | grid-column-end: time; 927 | } 928 | } 929 | 930 | &.normal { 931 | grid-template-areas: 932 | "thumbnail thumbnail empty1" 933 | "thumbnail thumbnail title" 934 | "empty2 time owner"; 935 | width: 100%; 936 | grid-template-columns: min-content min-content auto; 937 | gap: 5px; 938 | 939 | & img { 940 | height: 100px; 941 | grid-row-start: thumbnail; 942 | grid-row-end: time; 943 | grid-column-start: thumbnail; 944 | grid-column-end: time; 945 | 946 | @media (max-width: 450px) { 947 | height: 60px; 948 | } 949 | } 950 | } 951 | 952 | &.list { 953 | grid-template-columns: min-content auto; 954 | grid-template-areas: 955 | "thumbnail title" 956 | "thumbnail owner"; 957 | align-items: center; 958 | gap: 4px; 959 | 960 | &.active { 961 | background: var(--other-color); 962 | } 963 | 964 | & img { 965 | grid-area: thumbnail; 966 | width: 70px; 967 | } 968 | 969 | & .length { 970 | display: none; 971 | } 972 | } 973 | } 974 | 975 | .-comment-preview { 976 | display: flex; 977 | gap: 10px; 978 | align-items: center; 979 | 980 | & img { 981 | border-radius: var(--border-radius-size); 982 | height: 44px; 983 | } 984 | 985 | & .name { 986 | font-weight: 700; 987 | } 988 | 989 | & .text { 990 | user-select: text; 991 | } 992 | } 993 | 994 | .-owner-preview { 995 | grid-area: owner; 996 | display: grid; 997 | grid-template-columns: min-content auto min-content; 998 | grid-template-rows: 1fr 1fr; 999 | grid-template-areas: 1000 | "thumbnail name follow" 1001 | "thumbnail followers follow"; 1002 | gap: 0 20px; 1003 | 1004 | @media (max-width: 1000px) { 1005 | width: unset; 1006 | } 1007 | 1008 | & .thumbnail { 1009 | grid-area: thumbnail; 1010 | height: 48px; 1011 | aspect-ratio: 1; 1012 | border-radius: var(--border-radius-size); 1013 | } 1014 | 1015 | & .name { 1016 | grid-area: name; 1017 | width: 200px; 1018 | text-wrap: nowrap; 1019 | overflow: hidden; 1020 | 1021 | @media (max-width: 1000px) { 1022 | width: unset; 1023 | } 1024 | } 1025 | 1026 | & .followers { 1027 | grid-area: followers; 1028 | width: 200px; 1029 | text-wrap: nowrap; 1030 | overflow: hidden; 1031 | 1032 | @media (max-width: 1000px) { 1033 | width: unset; 1034 | } 1035 | } 1036 | 1037 | & .follow { 1038 | grid-area: follow; 1039 | border-radius: var(--border-radius-size); 1040 | border: none; 1041 | background: var(--other-color); 1042 | display: flex; 1043 | height: 32px; 1044 | width: 32px; 1045 | justify-content: center; 1046 | align-items: center; 1047 | align-self: center; 1048 | } 1049 | } 1050 | 1051 | #owner { 1052 | .banner { 1053 | .background { 1054 | width: 100%; 1055 | border-radius: var(--border-radius-size); 1056 | 1057 | &[banner="false"] { 1058 | display: none; 1059 | } 1060 | } 1061 | 1062 | &:has(.background[banner="false"]) .overlay { 1063 | top: 0px; 1064 | } 1065 | 1066 | .overlay { 1067 | position: relative; 1068 | width: 90%; 1069 | left: 5%; 1070 | top: calc(clamp(20px, 5vw, 150px) * -1); 1071 | display: flex; 1072 | gap: clamp(5px, 1vw, 20px); 1073 | align-items: center; 1074 | background: var(--seconder-background-color); 1075 | padding: 5px; 1076 | border-radius: var(--border-radius-size); 1077 | 1078 | .thumbnail { 1079 | --size: clamp(64px, 15%, 250px); 1080 | width: var(--size); 1081 | height: var(--size); 1082 | border-radius: var(--border-radius-size); 1083 | } 1084 | 1085 | .info { 1086 | .name { 1087 | font-size: clamp(20px,2.5vw,48px); 1088 | font-weight: bold; 1089 | } 1090 | .inline-info { 1091 | display: flex; 1092 | flex-direction: row; 1093 | gap: 5px; 1094 | 1095 | & > *:not(:last-child)::after{ 1096 | content: " \002022 "; 1097 | } 1098 | } 1099 | .description { 1100 | user-select: text; 1101 | } 1102 | .follow { 1103 | position: absolute; 1104 | top: 12px; 1105 | right: 12px; 1106 | border-radius: var(--border-radius-size); 1107 | border: none; 1108 | background: var(--other-color); 1109 | display: flex; 1110 | height: 32px; 1111 | width: 32px; 1112 | justify-content: center; 1113 | align-items: center; 1114 | align-self: center; 1115 | } 1116 | } 1117 | } 1118 | } 1119 | } 1120 | 1121 | input.switch { 1122 | appearance: unset; 1123 | background: var(--seconder-background-color); 1124 | width: 37.5px; 1125 | height: 15px; 1126 | border-radius: 15px; 1127 | 1128 | &::after { 1129 | content: ""; 1130 | display: block; 1131 | position: relative; 1132 | width: 20px; 1133 | height: 20px; 1134 | transform: translate(-2.5px, -2.5px); 1135 | background: var(--hover-color); 1136 | border-radius: 50%; 1137 | transition-duration: 100ms; 1138 | margin-top: 4px; 1139 | } 1140 | 1141 | &.icons::after { 1142 | content: attr(off_icon); 1143 | font-family: "Material Symbols Rounded"; 1144 | line-height: 16px; 1145 | font-size: 16px; 1146 | box-sizing: border-box; 1147 | padding: 2px; 1148 | } 1149 | 1150 | &:checked::after { 1151 | transform: translate(20px, -2.5px); 1152 | } 1153 | 1154 | &.icons:checked::after { 1155 | content: attr(on_icon); 1156 | } 1157 | } 1158 | 1159 | 1160 | .info_with_kaomoji { 1161 | display: flex; 1162 | flex-direction: column; 1163 | align-items: center; 1164 | 1165 | & .kaomoji { 1166 | font-size: 48px; 1167 | } 1168 | } 1169 | 1170 | @keyframes loading-circle { 1171 | from { 1172 | transform: translate(-50%, -50%) rotateZ(0deg); 1173 | } 1174 | to { 1175 | transform: translate(-50%, -50%) rotateZ(360deg); 1176 | } 1177 | } 1178 | 1179 | button.global-action { 1180 | border-radius: var(--border-radius-size); 1181 | border: none; 1182 | background: var(--seconder-color); 1183 | } --------------------------------------------------------------------------------