From 43a3ebf475b0ff367eacf6898abf487e86650c25 Mon Sep 17 00:00:00 2001 From: wukko Date: Sat, 5 Aug 2023 00:43:12 +0600 Subject: [PATCH 01/16] 7.0: ui refresh and more --- ....yml.example => docker-compose.example.yml | 0 package.json | 2 +- src/cobalt.js | 10 +- src/core/api.js | 142 +++--- src/core/web.js | 84 +++- src/front/cobalt.css | 329 ++++++++----- src/front/cobalt.js | 85 +++- src/localization/languages/en.json | 39 +- src/localization/languages/ru.json | 34 +- src/localization/manager.js | 4 +- src/modules/build.js | 6 +- src/modules/changelog/changelog.json | 11 +- src/modules/changelog/changelogManager.js | 12 +- src/modules/config.js | 1 - src/modules/pageRender/elements.js | 107 +++-- src/modules/pageRender/onDemand.js | 11 +- src/modules/pageRender/page.js | 441 +++++++++++------- src/modules/processing/matchActionDecider.js | 1 + src/modules/processing/services/reddit.js | 20 +- src/modules/processing/servicesConfig.json | 2 +- src/modules/setup.js | 4 +- src/modules/sub/utils.js | 5 + src/test/tests.json | 14 +- 23 files changed, 838 insertions(+), 526 deletions(-) rename docker-compose.yml.example => docker-compose.example.yml (100%) diff --git a/docker-compose.yml.example b/docker-compose.example.yml similarity index 100% rename from docker-compose.yml.example rename to docker-compose.example.yml diff --git a/package.json b/package.json index 74aea82..aff6247 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "6.3.1", + "version": "7.0-dev", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/cobalt.js b/src/cobalt.js index e8f13b3..d03bca2 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -24,13 +24,13 @@ app.disable('x-powered-by'); await loadLoc(); -const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port)) -const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port)) +const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port)); +const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port)); if (apiMode) { - runAPI(express, app, gitCommit, gitBranch, __dirname); + runAPI(express, app, gitCommit, gitBranch, __dirname) } else if (webMode) { - await runWeb(express, app, gitCommit, gitBranch, __dirname); + await runWeb(express, app, gitCommit, gitBranch, __dirname) } else { - console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`)); + console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`)) } diff --git a/src/core/api.js b/src/core/api.js index c70dd41..113e549 100644 --- a/src/core/api.js +++ b/src/core/api.js @@ -4,40 +4,45 @@ import { randomBytes } from "crypto"; const ipSalt = randomBytes(64).toString('hex'); -import { appName, version } from "../modules/config.js"; +import { version } from "../modules/config.js"; import { getJSON } from "../modules/api.js"; import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js"; import stream from "../modules/stream/stream.js"; import loc from "../localization/manager.js"; -import { changelogHistory } from "../modules/pageRender/onDemand.js"; import { sha256 } from "../modules/sub/crypto.js"; -import { celebrationsEmoji } from "../modules/pageRender/elements.js"; import { verifyStream } from "../modules/stream/manage.js"; export function runAPI(express, app, gitCommit, gitBranch, __dirname) { - const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {}; + const corsConfig = process.env.cors === '0' ? { + origin: process.env.webURL, + optionsSuccessStatus: 200 + } : {}; const apiLimiter = rateLimit({ windowMs: 60000, max: 20, - standardHeaders: false, + standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => sha256(getIP(req), ipSalt), handler: (req, res, next, opt) => { - res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') }); - return; + return res.status(429).json({ + "status": "error", + "text": loc(languageCode(req), 'ErrorRateLimit') + }); } }); const apiLimiterStream = rateLimit({ windowMs: 60000, max: 25, - standardHeaders: false, + standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => sha256(getIP(req), ipSalt), handler: (req, res, next, opt) => { - res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') }); - return; + return res.status(429).json({ + "status": "error", + "text": loc(languageCode(req), 'ErrorRateLimit') + }); } }); @@ -55,45 +60,55 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }); app.use('/api/json', express.json({ verify: (req, res, buf) => { - try { - JSON.parse(buf); + let acceptCon = String(req.header('Accept')) === "application/json"; + if (acceptCon) { if (buf.length > 720) throw new Error(); - if (String(req.header('Content-Type')) !== "application/json") { - res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' }); - return; - } - if (String(req.header('Accept')) !== "application/json") { - res.status(400).json({ 'status': 'error', 'text': 'invalid accept header' }); - return; - } - } catch(e) { - res.status(400).json({ 'status': 'error', 'text': 'invalid json body.' }); - return; + JSON.parse(buf); + } else { + throw new Error(); } } })); + // handle express.json errors properly (https://github.com/expressjs/express/issues/4065) + app.use('/api/json', (err, req, res, next) => { + let errorText = "invalid json body"; + let acceptCon = String(req.header('Accept')) !== "application/json"; + if (err || acceptCon) { + if (acceptCon) errorText = "invalid accept header"; + return res.status(400).json({ + status: "error", + text: errorText + }); + } else { + next(); + } + }); app.post('/api/json', async (req, res) => { try { let lang = languageCode(req); - let j = apiJSON(0, { t: "Bad request" }); + let j = apiJSON(0, { t: "bad request" }); try { + let contentCon = String(req.header('Content-Type')) === "application/json"; let request = req.body; - if (request.url) { + if (contentCon && request.url) { request.dubLang = request.dubLang ? lang : false; + let chck = checkJSONPost(request); - j = chck ? await getJSON(chck["url"], lang, chck) : apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') }); + if (!chck) throw new Error(); + + j = await getJSON(chck["url"], lang, chck); } else { - j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') }); + j = apiJSON(0, { + t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink') + }); } } catch (e) { j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') }); } - res.status(j.status).json(j.body); - return; + return res.status(j.status).json(j.body); } catch (e) { - res.destroy(); - return + return res.destroy(); } }); @@ -105,49 +120,23 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { && req.query.h.toString().length === 64 && req.query.e.toString().length === 13) { let streamInfo = verifyStream(req.query.t, req.query.h, req.query.e); if (streamInfo.error) { - res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); - return; + return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); } if (req.query.p) { - res.status(200).json({ "status": "continue" }); - return; + return res.status(200).json({ + status: "continue" + }); } - stream(res, streamInfo); + return stream(res, streamInfo); } else { - let j = apiJSON(0, { t: "stream token, hmac, or expiry timestamp is missing." }) - res.status(j.status).json(j.body); - return; - } - break; - case 'onDemand': - if (req.query.blockId) { - let blockId = req.query.blockId.slice(0, 3); - let r, j; - switch(blockId) { - case "0": // changelog history - r = changelogHistory(); - j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" }) - break; - case "1": // celebrations emoji - r = celebrationsEmoji(); - j = r ? apiJSON(3, { t: r }) : false - break; - default: - j = apiJSON(0, { t: "couldn't find a block with this id" }) - break; - } - if (j.body) { - res.status(j.status).json(j.body) - } else { - res.status(204).end() - } - } else { - let j = apiJSON(0, { t: "no block id" }); - res.status(j.status).json(j.body) + let j = apiJSON(0, { + t: "stream token, hmac, or expiry timestamp is missing" + }) + return res.status(j.status).json(j.body); } break; case 'serverInfo': - res.status(200).json({ + return res.status(200).json({ version: version, commit: gitCommit, branch: gitBranch, @@ -158,13 +147,17 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }); break; default: - let j = apiJSON(0, { t: "unknown response type" }) - res.status(j.status).json(j.body); + let j = apiJSON(0, { + t: "unknown response type" + }) + return res.status(j.status).json(j.body); break; } } catch (e) { - res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') }); - return; + return res.status(500).json({ + status: "error", + text: loc(languageCode(req), 'ErrorCantProcess') + }); } }); app.get('/api/status', (req, res) => { @@ -178,6 +171,11 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) { }); app.listen(process.env.apiPort, () => { - console.log(`\n${Cyan(appName)} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\nURL: ${Cyan(`${process.env.apiURL}`)}\nPort: ${process.env.apiPort}\n`) + console.log(`\n` + + `${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + + `URL: ${Cyan(`${process.env.apiURL}`)}\n` + + `Port: ${process.env.apiPort}\n` + ) }); } diff --git a/src/core/web.js b/src/core/web.js index afde9fe..c2512c1 100644 --- a/src/core/web.js +++ b/src/core/web.js @@ -1,20 +1,18 @@ -import { appName, genericUserAgent, version } from "../modules/config.js"; -import { languageCode } from "../modules/sub/utils.js"; +import { genericUserAgent, version } from "../modules/config.js"; +import { apiJSON, languageCode } from "../modules/sub/utils.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js"; + import { buildFront } from "../modules/build.js"; import findRendered from "../modules/pageRender/findRendered.js"; -// * will be removed in the future -import cors from "cors"; -// * +import { celebrationsEmoji } from "../modules/pageRender/elements.js"; +import { changelogHistory } from "../modules/pageRender/onDemand.js"; export async function runWeb(express, app, gitCommit, gitBranch, __dirname) { - await buildFront(gitCommit, gitBranch); + const startTime = new Date(); + const startTimestamp = Math.floor(startTime.getTime()); - // * will be removed in the future - const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {}; - app.use('/api/:type', cors(corsConfig)); - // * + await buildFront(gitCommit, gitBranch); app.use('/', express.static('./build/min')); app.use('/', express.static('./src/front')); @@ -23,29 +21,67 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) { try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') } next(); }); + app.get('/onDemand', (req, res) => { + try { + if (req.query.blockId) { + let blockId = req.query.blockId.slice(0, 3); + let r, j; + switch(blockId) { + // changelog history + case "0": + r = changelogHistory(); + j = r ? apiJSON(3, { t: r }) : apiJSON(0, { + t: "couldn't render this block, please try again!" + }) + break; + // celebrations emoji + case "1": + r = celebrationsEmoji(); + j = r ? apiJSON(3, { t: r }) : false + break; + default: + j = apiJSON(0, { + t: "couldn't find a block with this id" + }) + break; + } + if (j.body) { + return res.status(j.status).json(j.body); + } else { + return res.status(204).end(); + } + } else { + return res.status(400).json({ + status: "error", + text: "couldn't render this block, please try again!" + }); + } + } catch (e) { + return res.status(400).json({ + status: "error", + text: "couldn't render this block, please try again!" + }) + } + }); app.get("/status", (req, res) => { - res.status(200).end() + return res.status(200).end() }); app.get("/", (req, res) => { - res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`) + return res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`) }); app.get("/favicon.ico", (req, res) => { - res.sendFile(`${__dirname}/src/front/icons/favicon.ico`) + return res.sendFile(`${__dirname}/src/front/icons/favicon.ico`) }); - // * will be removed in the future - app.get("/api/*", (req, res) => { - res.redirect(308, process.env.apiURL.slice(0, -1) + req.url) - }); - app.post("/api/*", (req, res) => { - res.redirect(308, process.env.apiURL.slice(0, -1) + req.url) - }); - // * app.get("/*", (req, res) => { - res.redirect('/') + return res.redirect('/') }); app.listen(process.env.webPort, () => { - let startTime = new Date(); - console.log(`\n${Cyan(appName)} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.webURL}`)}\nPort: ${process.env.webPort}\n`) + console.log(`\n` + + `${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` + + `Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` + + `URL: ${Cyan(`${process.env.webURL}`)}\n` + + `Port: ${process.env.webPort}\n` + ) }) } diff --git a/src/front/cobalt.css b/src/front/cobalt.css index 7808b58..260d561 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -3,6 +3,8 @@ --without-padding: calc(100% - 4rem); --border-15: 0.15rem solid var(--accent); --border-10: 0.1rem solid var(--accent); + --inset-focus: 0 0 0 0.1rem var(--accent) inset; + --inset-focus-inv: 0 0 0 0.15rem var(--background) inset; --font-mono: 'Noto Sans Mono', 'Consolas', 'SF Mono', monospace; --padding-1: 0.75rem; --line-height: 1.65rem; @@ -20,6 +22,7 @@ --accent-button: rgb(25, 25, 25); --accent-button-elevated: rgb(42, 42, 42); --glass: rgba(25, 25, 25, 0.85); + --glass-lite: rgba(25, 25, 25, 0.98); --subbackground: rgb(10, 10, 10); --background: rgb(0, 0, 0); } @@ -34,6 +37,7 @@ --accent-button: rgb(225, 225, 225); --accent-button-elevated: rgb(210, 210, 210); --glass: rgba(230, 230, 230, 0.85); + --glass-lite: rgba(230, 230, 230, 0.98); --subbackground: rgb(240, 240, 240); --background: rgb(255, 255, 255); } @@ -47,6 +51,7 @@ --accent-button: rgb(25, 25, 25); --accent-button-elevated: rgb(42, 42, 42); --glass: rgba(25, 25, 25, 0.85); + --glass-lite: rgba(25, 25, 25, 0.98); --subbackground: rgb(10, 10, 10); --background: rgb(0, 0, 0); } @@ -59,6 +64,7 @@ --accent-button: rgb(225, 225, 225); --accent-button-elevated: rgb(210, 210, 210); --glass: rgba(230, 230, 230, 0.85); + --glass-lite: rgba(230, 230, 230, 0.98); --subbackground: rgb(240, 240, 240); --background: rgb(255, 255, 255); } @@ -74,6 +80,12 @@ body { overflow: hidden; -ms-overflow-style: none; scrollbar-width: none; + height: calc(100% + env(safe-area-inset-top)/2); +} +#home { + position: fixed; + width: 100%; + height: 100%; } a { color: var(--accent); @@ -150,12 +162,17 @@ input[type="text"], [type="text"] { border-radius: 0; } +.glass-bkg { + background: var(--glass); + backdrop-filter: blur(7px); + -webkit-backdrop-filter: blur(7px); +} .desktop button:hover, .desktop .switch:hover, .desktop .checkbox:hover, .desktop .text-to-copy:hover, .desktop .collapse-header:hover, -.desktop #close-button:hover { +.desktop #back-button:hover { background: var(--accent-hover); box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset; cursor: pointer; @@ -243,7 +260,7 @@ button:active, } .box { background: var(--background); - border: var(--border-15); + border: var(--glass) solid .2rem; color: var(--accent); } #url-input-area { @@ -284,13 +301,14 @@ button:active, cursor: not-allowed; } #footer { - bottom: 0.8rem; + bottom: 0; + width: 100%; position: absolute; - left: 50%; - transform: translate(-50%, -50%); + display: flex; + justify-content: center; + padding-bottom: calc(env(safe-area-inset-bottom)/2 + 2rem); font-size: 0.9rem; text-align: center; - width: auto; } #cobalt-main-box #bottom, #footer-buttons, @@ -343,27 +361,58 @@ button:active, visibility: hidden; position: fixed; height: auto; - width: 32%; + width: 36%; z-index: 999; - padding: 2rem; font-size: 0.9rem; - max-height: 85%; + max-height: 95%; + opacity: 0; + transform: translate(-50%,-48%)scale(.95); +} +.popup.visible { + visibility: visible; + opacity: 1; + transform: translate(-50%, -50%); + transition: transform 0.1s ease-in-out, opacity 0.1s ease-in-out; +} +#popup-backdrop { + visibility: hidden; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 998; + opacity: 0; + background-color: var(--background); +} +#popup-backdrop.visible { + visibility: visible; + opacity: 0.5; + transition: opacity 0.1s ease-in-out; } .popup.small { width: 20%; - background: var(--glass); - backdrop-filter: blur(7px); - -webkit-backdrop-filter: blur(7px); box-shadow: 0px 0px 80px 0px var(--accent-hover); - padding: 1.7rem; border: var(--accent-highlight) solid 0.15rem; + padding: 1.7rem; + transform: translate(-50%,-50%)scale(.95); + pointer-events: all; +} +.popup.small.visible { + transform: translate(-50%, -50%); +} +.popup.small #popup-header-contents, +.popup.small .popup-content-inner, +.popup.small #popup-header { + padding: 0; +} +.popup.small #popup-header { + position: relative; + border: none; } .popup.small #popup-title { margin-bottom: .2rem; } -.popup.small #popup-header { - padding-top: 0; -} .popup.small .explanation { margin-bottom: 0.8rem; } @@ -371,31 +420,22 @@ button:active, background: var(--accent); color: var(--background); } -#popup-backdrop { - opacity: 0.5; - background-color: var(--background); - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 998; -} .popup.scrollable { - height: 85%; + height: 95%; } .scrollable .bottom-link { padding-bottom: 2rem; } .changelog-subtitle { - font-size: 1.1rem; + font-size: 1.3rem; padding-bottom: var(--gap-no-icon); } .changelog-banner { + position: relative; width: 100%; max-height: 300px; min-height: 160px; - margin-bottom: 1.65rem; + margin-bottom: 1rem; float: left; } .changelog-img { @@ -404,6 +444,20 @@ button:active, height: inherit; max-height: inherit; } +.changelog-tags { + display: inline-flex; + align-items: center; + gap: 0.7rem; + padding-bottom: 0.7rem; +} +.changelog-tag-version { + font-size: 1rem; + padding: 0.15rem 0.45rem; +} +.changelog-tag-date { + color: var(--accent-subtext); + font-size: .8rem; +} .nowrap { white-space: nowrap; } @@ -429,25 +483,10 @@ button:active, } #popup-title { font-size: 1.5rem; - margin-bottom: 0.5rem; line-height: 1.85em; display: flex; align-items: center; } -#popup-footer { - bottom: 0; - position: fixed; - margin-bottom: 1.5rem; - background: var(--background); - width: var(--without-padding); -} -.popup-footer-content { - font-size: 0.8rem; - line-height: var(--line-height); - color: var(--accent-subtext); - border-top: 0.05rem solid var(--accent-subtext); - padding-top: 0.4rem; -} #popup-above-title { color: var(--accent-subtext); font-size: 0.8rem; @@ -455,19 +494,27 @@ button:active, #popup-content { overflow-x: hidden; overflow-y: auto; - height: var(--without-padding); + height: 100%; scrollbar-width: none; } +.popup-content-inner, +.tab-content-settings { + padding-top: calc(env(safe-area-inset-top)/2 + 4.9rem); + padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem); +} +.tab-content-settings, +#tab-about-about .popup-content-inner { + padding-top: calc(env(safe-area-inset-top)/2 + 6.2rem);; +} .bullpadding { padding-left: 0.58rem; } #popup-header { - position: relative; + position: absolute; z-index: 999; - padding-top: 0.8rem; -} -#popup-content.with-footer { - margin-bottom: 3rem; + padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem); + width: 100%; + border-bottom: var(--accent-highlight) solid 0.1rem; } .settings-category { padding-bottom: 1rem; @@ -538,15 +585,24 @@ button:active, .switch.space-right { margin-right: var(--padding-1); } -.switch[data-enabled="true"] { +.switch:focus { + box-shadow: var(--inset-focus) inset; +} +#popup-tabs .switch { + background: unset; +} +.switch[data-enabled="true"], +#popup-tabs .switch[data-enabled="true"] { color: var(--background); background: var(--accent); cursor: default; - z-index: 999 } .switch[data-enabled="true"]:hover { background: var(--accent); } +.switch[data-enabled="true"]:focus { + box-shadow: var(--inset-focus-inv) inset; +} .switches { display: flex; width: auto; @@ -575,18 +631,14 @@ button:active, padding: var(--padding-1); overflow: auto; } -#close-button { - max-width: 2.6rem; - margin-left: var(--padding-1); - border: var(--border-15); - color: var(--accent); - padding: 0.3rem 0.75rem 0.5rem; +#back-button { + padding: 0; + background: none; + max-width: 4rem; + font-size: 1rem; } -#close-button.up { - float: right; - position: absolute; - right: 0; - height: 2.6rem; +#back-button svg path { + fill: var(--accent); } .popup-tab-content { display: none; @@ -594,23 +646,32 @@ button:active, #popup-tabs { z-index: 999; bottom: 0; - position: relative; + position: absolute; width: 100%; + padding-top: 0.2rem; + padding-bottom: 1.7rem; + border-top: var(--accent-highlight) solid 0.1rem; } -.popup-tabs { - margin-top: 0.9rem; +.popup-tabs-child { + width: 100%; + padding: 0 0.2rem; } -.emoji { +.emoji, svg { margin-right: 0.4rem; user-select: none; -webkit-user-select: none; pointer-events: none; } +.emoji { + margin-right: 0.4rem; +} .picker-image { object-fit: cover; width: inherit; height: inherit; cursor: pointer; + user-select: all; + -webkit-user-select: all; } .picker-image-container { width: 8rem; @@ -631,6 +692,8 @@ button:active, justify-content: space-between; flex-wrap: wrap; align-content: space-around; + padding-top: calc(env(safe-area-inset-top)/2 + 7.6rem); + padding-bottom: calc(env(safe-area-inset-bottom)/2 + 4.8rem); } #picker-holder.various { justify-content: left; @@ -642,7 +705,7 @@ button:active, height: 100%; width: 100%; position: absolute; - z-index: 9999; + z-index: 99; } .picker-element-name { position: absolute; @@ -706,41 +769,67 @@ button:active, #about-donate-footer { box-shadow: 0 0 0 0.1rem var(--accent) inset; } -.popup-tabs-child { - width: 100%; +.popup-content-inner, +.tab-content-settings, +#popup-header-contents { + padding-left: 1rem; + padding-right: 1rem; } .urgent-notice { - top: 1.7rem; - width: auto; - text-align: left; + width: 100%; + text-align: center; position: absolute; cursor: pointer; display: flex; justify-content: center; align-items: center; + padding-top: calc(env(safe-area-inset-top) + 1rem); +} +.no-transparency .glass-bkg { + background: var(--glass-lite); + backdrop-filter: none; + -webkit-backdrop-filter: none; +} +.no-animation .popup, +.no-animation #popup-backdrop { + transition: none; +} +#floating-notification-area { + visibility: visible; + z-index: 999999; + position: absolute; + display: flex; + justify-content: center; + width: 100%; + padding-top: 2rem; +} +.floating-notification { + text-align: center; + padding: 0.6rem 1.2rem; + background: var(--accent-hover-elevated); + display: flex; + box-shadow: 0 0 20px 10px var(--accent-hover); + font-size: 0.85rem; +} +.popup-from-bottom { + position: fixed; + width: 100%; + height: 100%; + bottom: 0; + z-index: 999; + visibility: hidden; + pointer-events: none; +} +.popup-from-bottom.visible { + visibility: visible; } /* adapt the page according to screen size */ -@media screen and (min-width: 2300px) { - html { - zoom: 130%; - } -} -@media screen and (min-width: 3840px) { - html { - zoom: 180%; - } -} -@media screen and (min-width: 5000px) { - html { - zoom: 300%; - } -} @media screen and (max-width: 1550px) { .popup.small { width: 25% } .popup { - width: 35%; + width: 40%; } } @media screen and (max-width: 1440px) { @@ -751,12 +840,12 @@ button:active, width: 30% } .popup { - width: 40%; + width: 45%; } } @media screen and (max-width: 1300px) { .popup { - width: 46%; + width: 50%; } } @media screen and (max-width: 1200px) { @@ -767,7 +856,7 @@ button:active, width: 35% } .popup { - width: 50%; + width: 55%; } } @media screen and (max-width: 1025px) { @@ -781,23 +870,12 @@ button:active, width: 60%; } } -@media screen and (max-height: 605px) { +@media screen and (max-width: 850px) { .popup { - height: 80% - } - .popup.small { - height: auto; - } - .bottom-link { - padding-bottom: 2rem; + width: 75%; } } /* mobile page */ -@media screen and (max-width: 720px) { - #cobalt-main-box, #footer { - width: 90%; - } -} @media screen and (max-width: 499px) { .tab { font-size: 0!important; @@ -805,9 +883,6 @@ button:active, .tab .emoji { margin-right: 0; } - #cobalt-main-box, #footer { - width: 90%; - } .checkbox { width: calc(100% - 1.3rem); } @@ -894,6 +969,9 @@ button:active, } } @media screen and (max-width: 720px) { + #cobalt-main-box { + width: calc(100% - (0.7rem * 2)); + } #cobalt-main-box #bottom { flex-direction: column-reverse; } @@ -901,12 +979,13 @@ button:active, width: 100%; } #footer { - bottom: 4.9%; - transform: translate(-50%, 0%); + padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); } #footer-buttons { flex-direction: column; align-items: stretch; + width: 100%; + padding: 0 0.7rem; } .footer-pair .footer-button { width: 100%!important; @@ -924,7 +1003,7 @@ button:active, gap: var(--gap); } .urgent-notice { - width: 100%; + padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem); } .popup.small { width: calc(100% - 1.7rem * 2); @@ -936,7 +1015,12 @@ button:active, position: absolute; border: none; border-top: var(--accent-highlight) solid 0.15rem; - padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.7rem) + padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.7rem); + transform: none; + } + .popup.small.visible { + transform: none; + transition: opacity 0.1s ease-in-out; } .popup.small #popup-header { background: none; @@ -949,7 +1033,6 @@ button:active, } #picker-holder.various { flex-wrap: wrap; - align-content: left; gap: 0; overflow-x: hidden; overflow-y: scroll; @@ -966,15 +1049,27 @@ button:active, } .popup, .popup.scrollable { border: none; - width: 90%; - height: 95%; + width: 100%; + height: 100%; max-height: 100%; } + .popup.center { + top: unset; + left: unset; + transform: unset; + } + #popup-tabs { + padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem); + } .bottom-link { padding-bottom: 2rem; } - .popup-tabs { - margin-top: .3rem; + .popup-content-inner, + .tab-content-settings, + .popup-tabs-child, + #popup-header-contents { + padding-left: 0.7rem; + padding-right: 0.7rem; } } @media screen and (max-width: 400px) { diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 7a624aa..6d09baf 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,6 +1,8 @@ const ua = navigator.userAgent.toLowerCase(); const isIOS = ua.match("iphone os"); const isMobile = ua.match("android") || ua.match("iphone os"); +const isFirefox = ua.match("firefox/"); +const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103; const version = 31; const regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); const notification = `
`; @@ -14,10 +16,11 @@ const switchers = { "vimeoDash": ["false", "true"], "audioMode": ["false", "true"] }; -const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"]; +const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio", "reduceTransparency", "disableAnimations"]; const exceptions = { // used for mobile devices "vQuality": "720" }; +const bottomPopups = ["error", "download"] let store = {}; @@ -156,16 +159,18 @@ function notificationCheck(type) { function hideAllPopups() { let filter = document.getElementsByClassName('popup'); for (let i = 0; i < filter.length; i++) { - filter[i].style.visibility = "hidden"; + filter[i].classList.remove("visible"); } eid("picker-holder").innerHTML = ''; eid("picker-download").href = '/'; - eid("picker-download").style.visibility = "hidden"; - eid("popup-backdrop").style.visibility = "hidden"; + eid("picker-download").classList.remove("visible"); + eid("popup-backdrop").classList.remove("visible"); + store.isPopupOpen = false; } function popup(type, action, text) { if (action === 1) { hideAllPopups(); // hide the previous popup before showing a new one + store.isPopupOpen = true; switch (type) { case "about": let tabId = sGet("seenAbout") ? "changelog" : "about"; @@ -192,7 +197,7 @@ function popup(type, action, text) { if (!eid("popup-picker").classList.contains("scrollable")) eid("popup-picker").classList.add("scrollable"); if (eid("picker-holder").classList.contains("various")) eid("picker-holder").classList.remove("various"); eid("picker-download").href = text.audio; - eid("picker-download").style.visibility = "visible" + eid("picker-download").classList.add("visible"); for (let i in text.arr) { eid("picker-holder").innerHTML += `` } @@ -206,12 +211,12 @@ function popup(type, action, text) { let s = text.arr[i], item; switch (s.type) { case "video": - item = `
VIDEO ${Number(i)+1}
` + item = `
VIDEO ${Number(i)+1}
` break; } eid("picker-holder").innerHTML += item } - eid("picker-download").style.visibility = "hidden"; + eid("picker-download").classList.remove("visible"); break; } break; @@ -219,14 +224,17 @@ function popup(type, action, text) { break; } } else { + store.isPopupOpen = false; if (type === "picker") { eid("picker-download").href = '/'; - eid("picker-download").style.visibility = "hidden" + eid("picker-download").classList.remove("visible"); eid("picker-holder").innerHTML = '' } } - eid("popup-backdrop").style.visibility = vis(action); - eid(`popup-${type}`).style.visibility = vis(action); + if (bottomPopups.includes(type)) eid(`popup-${type}-container`).classList.toggle("visible"); + eid("popup-backdrop").classList.toggle("visible"); + eid(`popup-${type}`).classList.toggle("visible"); + eid(`popup-${type}`).focus(); } function changeSwitcher(li, b) { if (b) { @@ -249,15 +257,12 @@ function checkbox(action) { sSet(action, !!eid(action).checked); switch(action) { case "alwaysVisibleButton": button(); break; + case "reduceTransparency": eid("cobalt-body").classList.toggle('no-transparency'); break; + case "disableAnimations": eid("cobalt-body").classList.toggle('no-animation'); break; } action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck(); } function loadSettings() { - try { - if (typeof(navigator.clipboard.readText) == "undefined") throw new Error(); - } catch (err) { - eid("paste").style.display = "none"; - } if (sGet("alwaysVisibleButton") === "true") { eid("alwaysVisibleButton").checked = true; eid("download-button").value = '>>' @@ -266,6 +271,12 @@ function loadSettings() { if (sGet("downloadPopup") === "true" && !isIOS) { eid("downloadPopup").checked = true; } + if (sGet("reduceTransparency") === "true" || isOldFirefox) { + eid("cobalt-body").classList.toggle('no-transparency'); + } + if (sGet("disableAnimations") === "true") { + eid("cobalt-body").classList.toggle('no-animation'); + } for (let i = 0; i < checkboxes.length; i++) { if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true; } @@ -312,7 +323,17 @@ async function pasteClipboard() { eid("url-input-area").value = t; download(eid("url-input-area").value); } - } catch (e) {} + } catch (e) { + let errorMessage = loc.featureErrorGeneric; + let doError = true; + e = String(e).toLowerCase(); + + if (e.includes("denied")) errorMessage = loc.clipboardErrorNoPermission; + if (e.includes("dismissed")) doError = false; + if (e.includes("function") && isFirefox) errorMessage = loc.clipboardErrorFirefox; + + if (doError) popup("error", 1, errorMessage); + } } async function download(url) { changeDownloadButton(2, '...'); @@ -409,7 +430,7 @@ async function download(url) { async function loadCelebrationsEmoji() { let bac = eid("about-footer").innerHTML; try { - let j = await fetch(`${apiURL}/api/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false }); + let j = await fetch(`/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false }); if (j && j.status === "success" && j.text) { eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('🐲', j.text); } @@ -426,7 +447,7 @@ async function loadOnDemand(elementId, blockId) { if (store.historyContent) { j = store.historyContent; } else { - await fetch(`${apiURL}/api/onDemand?blockId=${blockId}`).then(async(r) => { + await fetch(`/onDemand?blockId=${blockId}`).then(async(r) => { j = await r.json(); if (j && j.status === "success") { store.historyContent = j; @@ -461,14 +482,28 @@ window.onload = () => { button(); } } -eid("url-input-area").addEventListener("keydown", (event) => { - if (event.key === 'Escape') eid("url-input-area").value = ''; +eid("url-input-area").addEventListener("keydown", (e) => { button(); }) -eid("url-input-area").addEventListener("keyup", (event) => { - if (event.key === 'Enter') eid("download-button").click(); +eid("url-input-area").addEventListener("keyup", (e) => { + if (e.key === 'Enter') eid("download-button").click(); }) -document.onkeydown = (event) => { - if (event.key === "Tab" || event.ctrlKey) eid("url-input-area").focus(); - if (event.key === 'Escape') hideAllPopups(); +document.onkeydown = (e) => { + if (!store.isPopupOpen) { + if (e.ctrlKey || e.key === "/") eid("url-input-area").focus(); + if (e.key === "Escape" || e.key === "Clear" || e.key === "Delete") clearInput(); + + // top buttons + if (e.key === "D") pasteClipboard(); + if (e.key === "K") changeSwitcher('audioMode', 'false'); + if (e.key === "L") changeSwitcher('audioMode', 'true'); + + // popups + if (e.key === "B") popup('about', 1); + if (e.key === "N") popup('about', 1, 'donate'); + if (e.key === "M") popup('settings', 1); + + } else { + if (e.key === "Escape") hideAllPopups(); + } } diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 1f565a5..39c1381 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -4,21 +4,21 @@ "ContactLink": "create an issue on github" }, "strings": { + "AppTitleCobalt": "cobalt", "LinkInput": "paste the link here", - "AboutSummary": "{appName} is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!", + "AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!", "EmbedBriefDescription": "save what you love without ads, trackers, or other creepy bullshit.", "MadeWithLove": "made with <3 by wukko", "AccessibilityInputArea": "link input area", "AccessibilityOpenAbout": "open about popup", "AccessibilityDownloadButton": "download button", "AccessibilityOpenSettings": "open settings popup", - "AccessibilityClosePopup": "close the popup", "AccessibilityOpenDonate": "open donation popup", - "TitlePopupAbout": "what's {appName}?", + "TitlePopupAbout": "what's cobalt?", "TitlePopupSettings": "settings", "TitlePopupError": "uh-oh...", "TitlePopupChangelog": "what's new?", - "TitlePopupDonate": "support {appName}", + "TitlePopupDonate": "support cobalt", "TitlePopupDownload": "how to save?", "ErrorSomethingWentWrong": "something went wrong and i couldn't get anything for you. try again, but if issue persists, {ContactLink}.", "ErrorUnsupported": "it seems like this service is not supported yet or your link is invalid. have you pasted the right link?", @@ -29,8 +29,8 @@ "ErrorCouldntFetch": "i couldn't find anything about this link. check if it works and try again! some content may be region restricted, so keep that in mind.", "ErrorLengthLimit": "i can't process videos longer than {s} minutes, so pick something shorter instead!", "ErrorBadFetch": "something went wrong when i tried getting info about your link. are you sure it works? check if it does, and try again.", - "ErrorNoInternet": "there's no internet or {appName} api is temporarily unavailable. check your connection and try again.", - "ErrorCantConnectToServiceAPI": "i couldn't connect to the service api. maybe it's down, or {appName} got blocked. try again, but if error persists, {ContactLink}.", + "ErrorNoInternet": "there's no internet or cobalt api is temporarily unavailable. check your connection and try again.", + "ErrorCantConnectToServiceAPI": "i couldn't connect to the service api. maybe it's down, or cobalt got blocked. try again, but if error persists, {ContactLink}.", "ErrorEmptyDownload": "i don't see anything i could download by your link. try a different one!", "ErrorLiveVideo": "this is a live video, i am yet to learn how to look into future. wait for the stream to finish and try again!", "SettingsAppearanceSubtitle": "appearance", @@ -46,7 +46,7 @@ "AccessibilityEnableDownloadPopup": "ask what to do with downloads", "SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.", "LinkGitHubChanges": ">> see previous commits and contribute on github", - "NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.", + "NoScriptMessage": "cobalt uses javascript for api requests and interactive interface. you have to allow javascript to use this site. there are no pesty scripts, pinky promise.", "DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add this siri shortcut.\n2. press \"share\" above and select \"save to photos\" in appeared share sheet.\nif asked, review the permission request, and press \"always allow\".\n\nalternative method:\npress and hold the download button, hide the video preview, and select \"download linked file\" to download.\nthen, open safari downloads, select the file you downloaded, open share menu, and finally press \"save video\".", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.", "ClickToCopy": "press to copy", @@ -87,13 +87,12 @@ "MediaPickerTitle": "pick what to save", "MediaPickerExplanationPC": "click or right click to download what you want.", "MediaPickerExplanationPhone": "press or press and hold to download what you want.", - "MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save.", "TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!", "ErrorCantProcess": "i couldn't process your request :(\nyou can try again, but if issue persists, please {ContactLink}.", "ChangelogPressToHide": "collapse", "Donate": "donate", "DonateSub": "help it stay online", - "DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, meaning that it's completely free to use. turns out developing and keeping up a web service used by over 300,000 people is not that easy.\n\nif you ever found {appName} useful and want to help continue its development and maintenance consider chipping in! if you want to thank the developer, you can also do that via donations. every cent helps and is VERY appreciated!\n\n{appName}'s usage worldwide grows daily and i need to make up for it. as you can imagine, hosting costs grow progressively too. as a year 1 university student, i was not prepared for such expenses :(\n\ni am yet to earn anything from {appName}, everything goes back to users, so you're helping everyone who uses {appName}.\n\nyour help is more appreciated than ever!", + "DonateExplanation": "cobalt does not (and will never) serve ads or sell your data, meaning that it's completely free to use. turns out developing and keeping up a web service used by over 300,000 people is not that easy.\n\nif you ever found cobalt useful and want to help continue its development and maintenance consider chipping in! if you want to thank the developer, you can also do that via donations. every cent helps and is VERY appreciated!\n\ncobalt's usage worldwide grows daily and i need to make up for it. as you can imagine, hosting costs grow progressively too. as a year 1 university student, i was not prepared for such expenses :(\n\ni am yet to earn anything from cobalt, everything goes back to users, so you're helping everyone who uses cobalt.\n\nyour help is more appreciated than ever!", "DonateVia": "donate via", "DonateHireMe": "...or you can hire me :)", "SettingsVideoMute": "mute audio", @@ -103,24 +102,32 @@ "CollapseSupport": "support & source code", "CollapsePrivacy": "privacy policy", "ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!", - "FollowSupport": "keep in touch with {appName} for support, polls, news, and more:", - "SupportNote": "please note that questions and issues may take a while to respond to, there's only one person managing everything.", + "FollowSupport": "keep in touch with cobalt for support, polls, news, and more:", + "SupportNote": "please note that response may take a while, there's only one person managing everything.", "SourceCode": "report issues, explore source code, star or fork the repo:", - "PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, information about requested stream is temporarily stored in server's RAM for 20 seconds. as 20 seconds have passed, all previously stored information is permanently removed.\nno one (even me) has access to this data, because official {appName} codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check {appName}'s github repo yourself and see that everything is as stated.", + "PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, information about requested stream is temporarily stored in server's RAM for 20 seconds. as 20 seconds have passed, all previously stored information is permanently removed.\nno one (even me) has access to this data, because official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's github repo yourself and see that everything is as stated.", "ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", "ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.", "SettingsCodecSubtitle": "youtube codec", "SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.", "SettingsAudioDub": "youtube audio track", - "SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and {appName}) language is used.", + "SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.", "SettingsDubDefault": "original", "SettingsDubAuto": "auto", "SettingsVimeoPrefer": "vimeo downloads type", - "SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by {appName} into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.", + "SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by cobalt into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.", "ShareURL": "share", "ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!", "ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.", - "UrgentDonate": "{appName} needs your help!", - "PopupCloseDone": "done" + "UrgentDonate": "cobalt needs your help!", + "PopupCloseDone": "done", + "Accessibility": "accessibility", + "SettingsReduceTransparency": "reduce transparency", + "SettingsDisableAnimations": "disable animations", + "FeatureErrorGeneric": "your browser doesn't allow or support this feature. check if there are any updates available and try again!", + "ClipboardErrorFirefox": "you're using firefox where all clipboard reading functionality is disabled.\n\nyou can fix this by following steps listed here!\n\n...or you can paste the link manually instead.", + "ClipboardErrorNoPermission": "cobalt can't access the most recent item in your clipboard without your permission.\n\nif you don't want to give access, just paste the link manually instead.\n\nif you do, go to site settings and enable the clipboard permission.", + "SupportSelfTroubleshooting": "experiencing issues? try self-troubleshooting guide first!", + "AccessibilityGoBack": "go back and close the popup" } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 9f74c50..be93750 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -4,21 +4,21 @@ "ContactLink": "напиши об этом на github (можно на русском)" }, "strings": { + "AppTitleCobalt": "кобальт", "LinkInput": "вставь ссылку сюда", - "AboutSummary": "{appName} - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.", + "AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.", "EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.", "MadeWithLove": "сделано wukko, с <3", "AccessibilityInputArea": "зона вставки ссылки", "AccessibilityOpenAbout": "открыть окно с инфой", "AccessibilityDownloadButton": "кнопка скачивания", "AccessibilityOpenSettings": "открыть настройки", - "AccessibilityClosePopup": "закрыть окно", "AccessibilityOpenDonate": "сделать пожертвование", - "TitlePopupAbout": "что за {appName}?", + "TitlePopupAbout": "что за кобальт?", "TitlePopupSettings": "настройки", "TitlePopupError": "опаньки...", "TitlePopupChangelog": "что нового?", - "TitlePopupDonate": "поддержи {appName}", + "TitlePopupDonate": "поддержи кобальт", "TitlePopupDownload": "как сохранить?", "ErrorSomethingWentWrong": "что-то пошло совсем не так и у меня не получилось ничего для тебя достать. попробуй ещё раз, но если так и не получится, {ContactLink}.", "ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?", @@ -30,7 +30,7 @@ "ErrorLengthLimit": "я не могу обрабатывать видео длиннее чем {s} минут(ы), так что скачай что-нибудь покороче!", "ErrorBadFetch": "произошла какая-то ошибка при получении данных по твоей ссылке. убедись, что она работает, и попробуй ещё раз.", "ErrorNoInternet": "не получилось подключиться к серверу. проверь подключение к интернету и попробуй ещё раз!", - "ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же {appName} заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.", + "ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же кобальт заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.", "ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!", "ErrorLiveVideo": "я пока что не умею заглядывать в будущее, поэтому дождись окончания прямого эфира, и потом уже скачивай видео!", "SettingsAppearanceSubtitle": "внешний вид", @@ -46,13 +46,13 @@ "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", "LinkGitHubChanges": ">> смотри предыдущие изменения на github", - "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", + "NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", "DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь этот сценарий siri.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.", "ClickToCopy": "нажми, чтобы скопировать", "Download": "скачать", "CopyURL": "скопировать", - "AboutTab": "о {appName}", + "AboutTab": "о кобальте", "ChangelogTab": "изменения", "DonationsTab": "донаты", "SettingsVideoTab": "видео", @@ -93,7 +93,7 @@ "ChangelogPressToHide": "скрыть", "Donate": "задонатить", "DonateSub": "ты можешь помочь!", - "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что разработка и поддержка сервиса, которым пользуются более 300 тысяч людей, обходится довольно затратно.\n\nесли {appName} тебе помог и ты хочешь, чтобы он продолжал работать, то это можно сделать через донаты!\n\nиспользование {appName} по всему миру растёт с каждым днём, а в след за ним и стоимость хостинга. мне, как первокурснику, оплачивать такое в одиночку довольно трудно.\n\nя еще ничего не заработал на {appName}, всё возвращается обратно пользователям, так что ты помогаешь всем, кто использует {appName}.\n\nтвой донат на вес золота, ценится как никогда!", + "DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что разработка и поддержка сервиса, которым пользуются более 300 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал работать, то это можно сделать через донаты!\n\nиспользование кобальта по всему миру растёт с каждым днём, а в след за ним и стоимость хостинга. мне, как первокурснику, оплачивать такое в одиночку довольно трудно.\n\nя еще ничего не заработал на кобальте, всё возвращается обратно пользователям, так что ты помогаешь всем, кто использует кобальт.\n\nтвой донат на вес золота, ценится как никогда!", "DonateVia": "открыть", "DonateHireMe": "...или же ты можешь пригласить меня на работу :)", "SettingsVideoMute": "убрать аудио", @@ -103,24 +103,32 @@ "CollapseSupport": "поддержка и исходный код", "CollapsePrivacy": "политика конфиденциальности", "ServicesNote": "этот список далеко не финальный и постоянно пополняется. заглядывай сюда почаще, тогда точно будешь знать, что поддерживается!", - "FollowSupport": "оставайтесь на связи с {appName} для новостей, поддержки, участия в опросах, и многого другого:", + "FollowSupport": "оставайтесь на связи с кобальтом для новостей, поддержки, участия в опросах, и многого другого:", "SupportNote": "так как я один занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", - "PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - только твоё личное дело.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы такого типа загрузок.\n\nв этом случае данные о запрошенном стриме хранятся в ОЗУ сервера в течение 20 секунд. по истечении этого периода всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальный код {appName} не предоставляет такой возможности.\n\nты всегда можешь посмотреть исходный код {appName} и убедиться, что всё так, как описано.", + "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - только твоё личное дело.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы такого типа загрузок.\n\nв этом случае данные о запрошенном стриме хранятся в ОЗУ сервера в течение 20 секунд. по истечении этого периода всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальный код кобальта не предоставляет такой возможности.\n\nты всегда можешь посмотреть исходный код кобальт и убедиться, что всё так, как описано.", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "SettingsCodecSubtitle": "кодек для видео с youtube", "SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.", "SettingsAudioDub": "звуковая дорожка для видео с youtube", - "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера (и {appName}).", + "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.", "SettingsDubDefault": "оригинал", "SettingsDubAuto": "авто", "SettingsVimeoPrefer": "тип загрузок с vimeo", - "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: {appName} совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, {appName} скачает \"dash\".", + "SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".", "ShareURL": "поделиться", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!", "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "UrgentDonate": "нужна твоя помощь!", - "PopupCloseDone": "готово" + "PopupCloseDone": "готово", + "Accessibility": "общедоступность", + "SettingsReduceTransparency": "уменьшить прозрачность", + "SettingsDisableAnimations": "выключить анимации", + "FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!", + "ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным здесь\n\n...или же ты можешь просто вставить ссылку вручную.", + "ClipboardErrorNoPermission": "кобальт не может прочитать последний элемент в буфере обмена без твоего разрешения.\n\nесли ты не хочешь давать доступ, просто вставь ссылку вручную.\n\nну а если хочешь, то открой настройки сайта и разреши доступ на чтение буфера обмена.", + "SupportSelfTroubleshooting": "возникли проблемы? попробуй сначала исправить всё сам по этому гиду!", + "AccessibilityGoBack": "вернуться назад и закрыть окно" } } diff --git a/src/localization/manager.js b/src/localization/manager.js index e5eec9b..76b6873 100644 --- a/src/localization/manager.js +++ b/src/localization/manager.js @@ -1,5 +1,5 @@ import * as fs from "fs"; -import { appName, links, repo } from "../modules/config.js"; +import { links, repo } from "../modules/config.js"; import loadJson from "../modules/sub/loadJSON.js"; const locPath = './src/localization/languages'; @@ -16,7 +16,7 @@ export async function loadLoc() { } export function replaceBase(s) { - return s.replace(/\n/g, '
').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "•"); + return s.replace(/\n/g, '
').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{repo}/g, repo).replace(/\*;/g, "•"); } export function replaceAll(lang, str, string, replacement) { let s = replaceBase(str[string]) diff --git a/src/modules/build.js b/src/modules/build.js index a2515b0..887ffb5 100644 --- a/src/modules/build.js +++ b/src/modules/build.js @@ -1,14 +1,10 @@ import * as esbuild from "esbuild"; import * as fs from "fs"; import { loadLoc, languageList } from "../localization/manager.js"; +import { cleanHTML } from "./sub/utils.js"; import page from "./pageRender/page.js"; -function cleanHTML(html) { - let clean = html.replace(/ {4}/g, ''); - clean = clean.replace(/\n/g, ''); - return clean -} export async function buildFront(commitHash, branch) { try { // preload localization files diff --git a/src/modules/changelog/changelog.json b/src/modules/changelog/changelog.json index 35e39e9..3750fd6 100644 --- a/src/modules/changelog/changelog.json +++ b/src/modules/changelog/changelog.json @@ -1,11 +1,18 @@ { "current": { + "version": "7.0", + "date": "August 4, 2023", + "title": "wip: ui refresh and more!", + "banner": "cattired.webp", + "content": "hey beta testers, this changelog isn't final but i do want to highlight some changes here just to keep track of them. make sure to report all issues in the testing discord channel!\n\n(this changelog is not sorted as it usually is)\n\nservice improvements:\n*; fixed unexpected stream drop when downloading a silent reddit video with mute mode on.\n*; added support for new reddit audio link type.\n\nweb improvements:\n*; removed 6.0 api fallback.\n*; moved on demand blocks to web server, now changelog can be updated independently from preferred api server.\n*; all-new matte glass aesthetic, applied to revamped popup headers, tab selectors, and also small popups.\n*; optimized installed web app to look and act like a native app, especially on ios. !!!!please try this!!!!\n*; added ability to attach a date to changelog.\n*; refreshed the look of entire changelog tab: separated title and version/commit, made title bigger, evened out all paddings.\n*; popups now work without any weird workarounds, especially on mobile. they're clean and nice.\n*; homescreen now also works without any weird workarounds. it is also clean and nice.\n*; replaced close button with back button, moved it to left. it makes more sense.\n*; (kinda old but not in older changelog) absolutely reimagined error and download popups, consistent with the rest of refreshed design.\n*; reduced spacing, optimized css of almost all ui elements. should be even more consistent across platforms now.\n*; added interaction animations.\n*; added more accessibility options, put them all into one category. you can disable animations and transparency if you want to.\n*; added a link to self-troubleshooting guide to support expand list in about popup.\n*; renamed 2160p and 4320p to 4k and 8k respectfully for better clarity.\n*; cobalt now lets you know if your browser doesn't support clipboard api and helps you fix it.\n*; added ability to translate \"cobalt\" for twitter-like localization. in russian cobalt is now кобальт, that's the style i will be going with from now on.\n*; updated some localization strings.\n*; removed ability to change the app name dynamically in all locations. cobalt is a sustained product name.\n*; \n*; added more keyboard shorcuts:\nshift+d: paste and download,\nshift+k: auto mode,\nshift+L: audio mode,\nshift+b: about popup,\nshift+n: donate popup,\nshift+m: settings popup.\n\non top of existing ones:\nctrl+v (without focusing anything): paste the link;\nescape/delete/clear: clear url input area\nescape: close current popup;\n\nyour keyboard slightly represents cobalt's ui. let me know if you like these.\n\ninternal web improvements:\n*; cleaned up all related frontend modules, especially page.js. will add more in final changelog, i'm very tired.\n\napi improvements:\n*; now catching all json api related errors.\n*; moved on demand blocks to web server.\n*; now sending standard rate limiting headers.\n*; better readability in source.\n\nother improvements:\n*; renamed docker-compose.yml.example to docker-compose.example.yml for linting in code editors.\n*; added a wiki with wip troubleshooting guide on github.\n\nwhat doesn't work or works poorly:\n*; tiktok/twitter media pickers look like shit, they haven't been worked on yet. they also might not work at all on ios.\n*; unknown if scrolling within popups works properly on ios 16 (when installed as web app).\n*; \"ask how to save\" toggle is pressable on ios devices even though it shouldn't be.\n\nwhat will surely be added in coming days:\n*; list of all keyboard shortcuts, probably a popup opened with a little button in left or right corner of the screen.\n*; proper dropdown arrow for about tab dropdowns.\n*; dates for all older changelogs.\n*; ...more?" + }, + "history": [{ "version": "6.2", + "date": "June 27 2023", "title": "all network issues have been fixed!", "banner": "meowthhammer.webp", "content": "hey! there have been some hiccups in cobalt's stability lately, i was going through finals while trying to scale up the infrastructure, and that didn't really work out, lol.\nBUT i'm happy to announce that i've optimized all nodes! there should no longer be any networking issues.\n\nenjoy stable experience while i work in background to make cobalt even better :)\n\nhere's what's new in this update:\n*; better button contrast in both themes. \n*; button highlight in light theme now actually looks like a highlight.\n*; removed ip gate for streamables and updated privacy policy to reflect this change.\n*; streamable links now last for 20 seconds instead of 2 minutes.\n*; cleaned up stream verification algorithm. now the same function doesn't run 4 times in a row.\n*; removed deprecated way of hosting a cobalt instance.\n\nthank you for sticking with cobalt, and i hope you have a great day :D" - }, - "history": [{ + }, { "version": "6.0", "title": "better reliability, new infrastructure, pinterest support, and way more!", "banner": "catswitchboxes.webp", diff --git a/src/modules/changelog/changelogManager.js b/src/modules/changelog/changelogManager.js index 306c6e1..84d213f 100644 --- a/src/modules/changelog/changelogManager.js +++ b/src/modules/changelog/changelogManager.js @@ -6,8 +6,12 @@ let changelog = loadJSON('./src/modules/changelog/changelog.json') export default function(string) { try { switch (string) { + case "version": + return `v.${changelog["current"]["version"]}${ + changelog["current"]["date"] ? `· ${changelog["current"]["date"]}` : '' + }` case "title": - return `${changelog["current"]["version"]}: ${replaceBase(changelog["current"]["title"])}`; + return replaceBase(changelog["current"]["title"]); case "banner": return changelog["current"]["banner"] ? `updateBanners/${changelog["current"]["banner"]}` : false; case "content": @@ -15,9 +19,11 @@ export default function(string) { case "history": return changelog["history"].map((i) => { return { - title: `${i["version"]}: ${replaceBase(i["title"])}`, + title: replaceBase(i["title"]), + version: `v.${i["version"]}${ + i["date"] ? `· ${i["date"]}` : '' + }`, content: replaceBase(i["content"]), - version: i["version"], banner: i["banner"] ? `updateBanners/${i["banner"]}` : false, } }); diff --git a/src/modules/config.js b/src/modules/config.js index 00b9566..5268b8d 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -6,7 +6,6 @@ const servicesConfigJson = loadJson("./src/modules/processing/servicesConfig.jso export const services = servicesConfigJson.config, audioIgnore = servicesConfigJson.audioIgnore, - appName = packageJson.name, version = packageJson.version, streamLifespan = config.streamLifespan, maxVideoDuration = config.maxVideoDuration, diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index dd9861a..898d1f8 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -1,6 +1,10 @@ import { celebrations } from "../config.js"; import emoji from "../emoji.js"; +export const backButtonSVG = ` + +` + export function switcher(obj) { let items = ``; if (obj.name === "download") { @@ -19,26 +23,18 @@ export function switcher(obj) { ${obj.explanation ? `
${obj.explanation}
` : ``} ` } +export function checkbox(obj) { + let paddings = ["bottom-margin", "top-margin", "no-margin", "top-margin-only"]; + let checkboxes = ``; + for (let i = 0; i < obj.length; i++) { + let paddingClass = obj[i].padding && paddings.includes(obj[i].padding) ? ` ${obj[i].padding}` : ''; -export function checkbox(action, text, paddingType, aria) { - let paddingClass = ` ` - switch (paddingType) { - case 1: - paddingClass += "bottom-margin" - break; - case 2: - paddingClass += "top-margin" - break; - case 3: - paddingClass += "no-margin" - break; - case 4: - paddingClass += "top-margin-only" + checkboxes += `` } - return `` + return checkboxes } export function sep(paddingType) { let paddingClass = `` @@ -50,7 +46,7 @@ export function sep(paddingType) { return `
` } export function popup(obj) { - let classes = obj.classes ? obj.classes : [] + let classes = obj.classes ? obj.classes : []; let body = obj.body; if (Array.isArray(obj.body)) { body = `` @@ -65,37 +61,44 @@ export function popup(obj) { } } return ` - ${obj.standalone ? `