7.0: ui refresh and more

This commit is contained in:
wukko 2023-08-05 00:43:12 +06:00
parent 38ceb1be77
commit 43a3ebf475
23 changed files with 838 additions and 526 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "6.3.1", "version": "7.0-dev",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View file

@ -24,13 +24,13 @@ app.disable('x-powered-by');
await loadLoc(); await loadLoc();
const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (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)) const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port));
if (apiMode) { if (apiMode) {
runAPI(express, app, gitCommit, gitBranch, __dirname); runAPI(express, app, gitCommit, gitBranch, __dirname)
} else if (webMode) { } else if (webMode) {
await runWeb(express, app, gitCommit, gitBranch, __dirname); await runWeb(express, app, gitCommit, gitBranch, __dirname)
} else { } 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`))
} }

View file

@ -4,40 +4,45 @@ import { randomBytes } from "crypto";
const ipSalt = randomBytes(64).toString('hex'); 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 { getJSON } from "../modules/api.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js"; import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js";
import stream from "../modules/stream/stream.js"; import stream from "../modules/stream/stream.js";
import loc from "../localization/manager.js"; import loc from "../localization/manager.js";
import { changelogHistory } from "../modules/pageRender/onDemand.js";
import { sha256 } from "../modules/sub/crypto.js"; import { sha256 } from "../modules/sub/crypto.js";
import { celebrationsEmoji } from "../modules/pageRender/elements.js";
import { verifyStream } from "../modules/stream/manage.js"; import { verifyStream } from "../modules/stream/manage.js";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) { 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({ const apiLimiter = rateLimit({
windowMs: 60000, windowMs: 60000,
max: 20, max: 20,
standardHeaders: false, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt), keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => { handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') }); return res.status(429).json({
return; "status": "error",
"text": loc(languageCode(req), 'ErrorRateLimit')
});
} }
}); });
const apiLimiterStream = rateLimit({ const apiLimiterStream = rateLimit({
windowMs: 60000, windowMs: 60000,
max: 25, max: 25,
standardHeaders: false, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt), keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => { handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') }); return res.status(429).json({
return; "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({ app.use('/api/json', express.json({
verify: (req, res, buf) => { verify: (req, res, buf) => {
try { let acceptCon = String(req.header('Accept')) === "application/json";
JSON.parse(buf); if (acceptCon) {
if (buf.length > 720) throw new Error(); if (buf.length > 720) throw new Error();
if (String(req.header('Content-Type')) !== "application/json") { JSON.parse(buf);
res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' }); } else {
return; throw new Error();
}
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;
} }
} }
})); }));
// 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) => { app.post('/api/json', async (req, res) => {
try { try {
let lang = languageCode(req); let lang = languageCode(req);
let j = apiJSON(0, { t: "Bad request" }); let j = apiJSON(0, { t: "bad request" });
try { try {
let contentCon = String(req.header('Content-Type')) === "application/json";
let request = req.body; let request = req.body;
if (request.url) { if (contentCon && request.url) {
request.dubLang = request.dubLang ? lang : false; request.dubLang = request.dubLang ? lang : false;
let chck = checkJSONPost(request); 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 { } else {
j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') }); j = apiJSON(0, {
t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink')
});
} }
} catch (e) { } catch (e) {
j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') }); j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') });
} }
res.status(j.status).json(j.body); return res.status(j.status).json(j.body);
return;
} catch (e) { } catch (e) {
res.destroy(); return res.destroy();
return
} }
}); });
@ -105,49 +120,23 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
&& req.query.h.toString().length === 64 && req.query.e.toString().length === 13) { && req.query.h.toString().length === 64 && req.query.e.toString().length === 13) {
let streamInfo = verifyStream(req.query.t, req.query.h, req.query.e); let streamInfo = verifyStream(req.query.t, req.query.h, req.query.e);
if (streamInfo.error) { if (streamInfo.error) {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body); return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
return;
} }
if (req.query.p) { if (req.query.p) {
res.status(200).json({ "status": "continue" }); return res.status(200).json({
return; status: "continue"
});
} }
stream(res, streamInfo); return stream(res, streamInfo);
} else { } else {
let j = apiJSON(0, { t: "stream token, hmac, or expiry timestamp is missing." }) let j = apiJSON(0, {
res.status(j.status).json(j.body); t: "stream token, hmac, or expiry timestamp is missing"
return; })
} return res.status(j.status).json(j.body);
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)
} }
break; break;
case 'serverInfo': case 'serverInfo':
res.status(200).json({ return res.status(200).json({
version: version, version: version,
commit: gitCommit, commit: gitCommit,
branch: gitBranch, branch: gitBranch,
@ -158,13 +147,17 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
}); });
break; break;
default: default:
let j = apiJSON(0, { t: "unknown response type" }) let j = apiJSON(0, {
res.status(j.status).json(j.body); t: "unknown response type"
})
return res.status(j.status).json(j.body);
break; break;
} }
} catch (e) { } catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') }); return res.status(500).json({
return; status: "error",
text: loc(languageCode(req), 'ErrorCantProcess')
});
} }
}); });
app.get('/api/status', (req, res) => { app.get('/api/status', (req, res) => {
@ -178,6 +171,11 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
}); });
app.listen(process.env.apiPort, () => { 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`
)
}); });
} }

View file

@ -1,20 +1,18 @@
import { appName, genericUserAgent, version } from "../modules/config.js"; import { genericUserAgent, version } from "../modules/config.js";
import { languageCode } from "../modules/sub/utils.js"; import { apiJSON, languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js"; import { Bright, Cyan } from "../modules/sub/consoleText.js";
import { buildFront } from "../modules/build.js"; import { buildFront } from "../modules/build.js";
import findRendered from "../modules/pageRender/findRendered.js"; import findRendered from "../modules/pageRender/findRendered.js";
// * will be removed in the future import { celebrationsEmoji } from "../modules/pageRender/elements.js";
import cors from "cors"; import { changelogHistory } from "../modules/pageRender/onDemand.js";
// *
export async function runWeb(express, app, gitCommit, gitBranch, __dirname) { 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 await buildFront(gitCommit, gitBranch);
const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {};
app.use('/api/:type', cors(corsConfig));
// *
app.use('/', express.static('./build/min')); app.use('/', express.static('./build/min'));
app.use('/', express.static('./src/front')); 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('/') } try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next(); 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) => { app.get("/status", (req, res) => {
res.status(200).end() return res.status(200).end()
}); });
app.get("/", (req, res) => { 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) => { 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) => { app.get("/*", (req, res) => {
res.redirect('/') return res.redirect('/')
}); });
app.listen(process.env.webPort, () => { app.listen(process.env.webPort, () => {
let startTime = new Date(); console.log(`\n` +
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`) `${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`
)
}) })
} }

View file

@ -3,6 +3,8 @@
--without-padding: calc(100% - 4rem); --without-padding: calc(100% - 4rem);
--border-15: 0.15rem solid var(--accent); --border-15: 0.15rem solid var(--accent);
--border-10: 0.1rem 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; --font-mono: 'Noto Sans Mono', 'Consolas', 'SF Mono', monospace;
--padding-1: 0.75rem; --padding-1: 0.75rem;
--line-height: 1.65rem; --line-height: 1.65rem;
@ -20,6 +22,7 @@
--accent-button: rgb(25, 25, 25); --accent-button: rgb(25, 25, 25);
--accent-button-elevated: rgb(42, 42, 42); --accent-button-elevated: rgb(42, 42, 42);
--glass: rgba(25, 25, 25, 0.85); --glass: rgba(25, 25, 25, 0.85);
--glass-lite: rgba(25, 25, 25, 0.98);
--subbackground: rgb(10, 10, 10); --subbackground: rgb(10, 10, 10);
--background: rgb(0, 0, 0); --background: rgb(0, 0, 0);
} }
@ -34,6 +37,7 @@
--accent-button: rgb(225, 225, 225); --accent-button: rgb(225, 225, 225);
--accent-button-elevated: rgb(210, 210, 210); --accent-button-elevated: rgb(210, 210, 210);
--glass: rgba(230, 230, 230, 0.85); --glass: rgba(230, 230, 230, 0.85);
--glass-lite: rgba(230, 230, 230, 0.98);
--subbackground: rgb(240, 240, 240); --subbackground: rgb(240, 240, 240);
--background: rgb(255, 255, 255); --background: rgb(255, 255, 255);
} }
@ -47,6 +51,7 @@
--accent-button: rgb(25, 25, 25); --accent-button: rgb(25, 25, 25);
--accent-button-elevated: rgb(42, 42, 42); --accent-button-elevated: rgb(42, 42, 42);
--glass: rgba(25, 25, 25, 0.85); --glass: rgba(25, 25, 25, 0.85);
--glass-lite: rgba(25, 25, 25, 0.98);
--subbackground: rgb(10, 10, 10); --subbackground: rgb(10, 10, 10);
--background: rgb(0, 0, 0); --background: rgb(0, 0, 0);
} }
@ -59,6 +64,7 @@
--accent-button: rgb(225, 225, 225); --accent-button: rgb(225, 225, 225);
--accent-button-elevated: rgb(210, 210, 210); --accent-button-elevated: rgb(210, 210, 210);
--glass: rgba(230, 230, 230, 0.85); --glass: rgba(230, 230, 230, 0.85);
--glass-lite: rgba(230, 230, 230, 0.98);
--subbackground: rgb(240, 240, 240); --subbackground: rgb(240, 240, 240);
--background: rgb(255, 255, 255); --background: rgb(255, 255, 255);
} }
@ -74,6 +80,12 @@ body {
overflow: hidden; overflow: hidden;
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
height: calc(100% + env(safe-area-inset-top)/2);
}
#home {
position: fixed;
width: 100%;
height: 100%;
} }
a { a {
color: var(--accent); color: var(--accent);
@ -150,12 +162,17 @@ input[type="text"],
[type="text"] { [type="text"] {
border-radius: 0; border-radius: 0;
} }
.glass-bkg {
background: var(--glass);
backdrop-filter: blur(7px);
-webkit-backdrop-filter: blur(7px);
}
.desktop button:hover, .desktop button:hover,
.desktop .switch:hover, .desktop .switch:hover,
.desktop .checkbox:hover, .desktop .checkbox:hover,
.desktop .text-to-copy:hover, .desktop .text-to-copy:hover,
.desktop .collapse-header:hover, .desktop .collapse-header:hover,
.desktop #close-button:hover { .desktop #back-button:hover {
background: var(--accent-hover); background: var(--accent-hover);
box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset; box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset;
cursor: pointer; cursor: pointer;
@ -243,7 +260,7 @@ button:active,
} }
.box { .box {
background: var(--background); background: var(--background);
border: var(--border-15); border: var(--glass) solid .2rem;
color: var(--accent); color: var(--accent);
} }
#url-input-area { #url-input-area {
@ -284,13 +301,14 @@ button:active,
cursor: not-allowed; cursor: not-allowed;
} }
#footer { #footer {
bottom: 0.8rem; bottom: 0;
width: 100%;
position: absolute; position: absolute;
left: 50%; display: flex;
transform: translate(-50%, -50%); justify-content: center;
padding-bottom: calc(env(safe-area-inset-bottom)/2 + 2rem);
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;
width: auto;
} }
#cobalt-main-box #bottom, #cobalt-main-box #bottom,
#footer-buttons, #footer-buttons,
@ -343,27 +361,58 @@ button:active,
visibility: hidden; visibility: hidden;
position: fixed; position: fixed;
height: auto; height: auto;
width: 32%; width: 36%;
z-index: 999; z-index: 999;
padding: 2rem;
font-size: 0.9rem; 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 { .popup.small {
width: 20%; width: 20%;
background: var(--glass);
backdrop-filter: blur(7px);
-webkit-backdrop-filter: blur(7px);
box-shadow: 0px 0px 80px 0px var(--accent-hover); box-shadow: 0px 0px 80px 0px var(--accent-hover);
padding: 1.7rem;
border: var(--accent-highlight) solid 0.15rem; 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 { .popup.small #popup-title {
margin-bottom: .2rem; margin-bottom: .2rem;
} }
.popup.small #popup-header {
padding-top: 0;
}
.popup.small .explanation { .popup.small .explanation {
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
} }
@ -371,31 +420,22 @@ button:active,
background: var(--accent); background: var(--accent);
color: var(--background); 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 { .popup.scrollable {
height: 85%; height: 95%;
} }
.scrollable .bottom-link { .scrollable .bottom-link {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
.changelog-subtitle { .changelog-subtitle {
font-size: 1.1rem; font-size: 1.3rem;
padding-bottom: var(--gap-no-icon); padding-bottom: var(--gap-no-icon);
} }
.changelog-banner { .changelog-banner {
position: relative;
width: 100%; width: 100%;
max-height: 300px; max-height: 300px;
min-height: 160px; min-height: 160px;
margin-bottom: 1.65rem; margin-bottom: 1rem;
float: left; float: left;
} }
.changelog-img { .changelog-img {
@ -404,6 +444,20 @@ button:active,
height: inherit; height: inherit;
max-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 { .nowrap {
white-space: nowrap; white-space: nowrap;
} }
@ -429,25 +483,10 @@ button:active,
} }
#popup-title { #popup-title {
font-size: 1.5rem; font-size: 1.5rem;
margin-bottom: 0.5rem;
line-height: 1.85em; line-height: 1.85em;
display: flex; display: flex;
align-items: center; 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 { #popup-above-title {
color: var(--accent-subtext); color: var(--accent-subtext);
font-size: 0.8rem; font-size: 0.8rem;
@ -455,19 +494,27 @@ button:active,
#popup-content { #popup-content {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
height: var(--without-padding); height: 100%;
scrollbar-width: none; 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 { .bullpadding {
padding-left: 0.58rem; padding-left: 0.58rem;
} }
#popup-header { #popup-header {
position: relative; position: absolute;
z-index: 999; z-index: 999;
padding-top: 0.8rem; padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem);
} width: 100%;
#popup-content.with-footer { border-bottom: var(--accent-highlight) solid 0.1rem;
margin-bottom: 3rem;
} }
.settings-category { .settings-category {
padding-bottom: 1rem; padding-bottom: 1rem;
@ -538,15 +585,24 @@ button:active,
.switch.space-right { .switch.space-right {
margin-right: var(--padding-1); 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); color: var(--background);
background: var(--accent); background: var(--accent);
cursor: default; cursor: default;
z-index: 999
} }
.switch[data-enabled="true"]:hover { .switch[data-enabled="true"]:hover {
background: var(--accent); background: var(--accent);
} }
.switch[data-enabled="true"]:focus {
box-shadow: var(--inset-focus-inv) inset;
}
.switches { .switches {
display: flex; display: flex;
width: auto; width: auto;
@ -575,18 +631,14 @@ button:active,
padding: var(--padding-1); padding: var(--padding-1);
overflow: auto; overflow: auto;
} }
#close-button { #back-button {
max-width: 2.6rem; padding: 0;
margin-left: var(--padding-1); background: none;
border: var(--border-15); max-width: 4rem;
color: var(--accent); font-size: 1rem;
padding: 0.3rem 0.75rem 0.5rem;
} }
#close-button.up { #back-button svg path {
float: right; fill: var(--accent);
position: absolute;
right: 0;
height: 2.6rem;
} }
.popup-tab-content { .popup-tab-content {
display: none; display: none;
@ -594,23 +646,32 @@ button:active,
#popup-tabs { #popup-tabs {
z-index: 999; z-index: 999;
bottom: 0; bottom: 0;
position: relative; position: absolute;
width: 100%; width: 100%;
padding-top: 0.2rem;
padding-bottom: 1.7rem;
border-top: var(--accent-highlight) solid 0.1rem;
} }
.popup-tabs { .popup-tabs-child {
margin-top: 0.9rem; width: 100%;
padding: 0 0.2rem;
} }
.emoji { .emoji, svg {
margin-right: 0.4rem; margin-right: 0.4rem;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
pointer-events: none; pointer-events: none;
} }
.emoji {
margin-right: 0.4rem;
}
.picker-image { .picker-image {
object-fit: cover; object-fit: cover;
width: inherit; width: inherit;
height: inherit; height: inherit;
cursor: pointer; cursor: pointer;
user-select: all;
-webkit-user-select: all;
} }
.picker-image-container { .picker-image-container {
width: 8rem; width: 8rem;
@ -631,6 +692,8 @@ button:active,
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
align-content: space-around; 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 { #picker-holder.various {
justify-content: left; justify-content: left;
@ -642,7 +705,7 @@ button:active,
height: 100%; height: 100%;
width: 100%; width: 100%;
position: absolute; position: absolute;
z-index: 9999; z-index: 99;
} }
.picker-element-name { .picker-element-name {
position: absolute; position: absolute;
@ -706,41 +769,67 @@ button:active,
#about-donate-footer { #about-donate-footer {
box-shadow: 0 0 0 0.1rem var(--accent) inset; box-shadow: 0 0 0 0.1rem var(--accent) inset;
} }
.popup-tabs-child { .popup-content-inner,
width: 100%; .tab-content-settings,
#popup-header-contents {
padding-left: 1rem;
padding-right: 1rem;
} }
.urgent-notice { .urgent-notice {
top: 1.7rem; width: 100%;
width: auto; text-align: center;
text-align: left;
position: absolute; position: absolute;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: 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 */ /* 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) { @media screen and (max-width: 1550px) {
.popup.small { .popup.small {
width: 25% width: 25%
} }
.popup { .popup {
width: 35%; width: 40%;
} }
} }
@media screen and (max-width: 1440px) { @media screen and (max-width: 1440px) {
@ -751,12 +840,12 @@ button:active,
width: 30% width: 30%
} }
.popup { .popup {
width: 40%; width: 45%;
} }
} }
@media screen and (max-width: 1300px) { @media screen and (max-width: 1300px) {
.popup { .popup {
width: 46%; width: 50%;
} }
} }
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
@ -767,7 +856,7 @@ button:active,
width: 35% width: 35%
} }
.popup { .popup {
width: 50%; width: 55%;
} }
} }
@media screen and (max-width: 1025px) { @media screen and (max-width: 1025px) {
@ -781,23 +870,12 @@ button:active,
width: 60%; width: 60%;
} }
} }
@media screen and (max-height: 605px) { @media screen and (max-width: 850px) {
.popup { .popup {
height: 80% width: 75%;
}
.popup.small {
height: auto;
}
.bottom-link {
padding-bottom: 2rem;
} }
} }
/* mobile page */ /* mobile page */
@media screen and (max-width: 720px) {
#cobalt-main-box, #footer {
width: 90%;
}
}
@media screen and (max-width: 499px) { @media screen and (max-width: 499px) {
.tab { .tab {
font-size: 0!important; font-size: 0!important;
@ -805,9 +883,6 @@ button:active,
.tab .emoji { .tab .emoji {
margin-right: 0; margin-right: 0;
} }
#cobalt-main-box, #footer {
width: 90%;
}
.checkbox { .checkbox {
width: calc(100% - 1.3rem); width: calc(100% - 1.3rem);
} }
@ -894,6 +969,9 @@ button:active,
} }
} }
@media screen and (max-width: 720px) { @media screen and (max-width: 720px) {
#cobalt-main-box {
width: calc(100% - (0.7rem * 2));
}
#cobalt-main-box #bottom { #cobalt-main-box #bottom {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
@ -901,12 +979,13 @@ button:active,
width: 100%; width: 100%;
} }
#footer { #footer {
bottom: 4.9%; padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem);
transform: translate(-50%, 0%);
} }
#footer-buttons { #footer-buttons {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
width: 100%;
padding: 0 0.7rem;
} }
.footer-pair .footer-button { .footer-pair .footer-button {
width: 100%!important; width: 100%!important;
@ -924,7 +1003,7 @@ button:active,
gap: var(--gap); gap: var(--gap);
} }
.urgent-notice { .urgent-notice {
width: 100%; padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem);
} }
.popup.small { .popup.small {
width: calc(100% - 1.7rem * 2); width: calc(100% - 1.7rem * 2);
@ -936,7 +1015,12 @@ button:active,
position: absolute; position: absolute;
border: none; border: none;
border-top: var(--accent-highlight) solid 0.15rem; 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 { .popup.small #popup-header {
background: none; background: none;
@ -949,7 +1033,6 @@ button:active,
} }
#picker-holder.various { #picker-holder.various {
flex-wrap: wrap; flex-wrap: wrap;
align-content: left;
gap: 0; gap: 0;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
@ -966,15 +1049,27 @@ button:active,
} }
.popup, .popup.scrollable { .popup, .popup.scrollable {
border: none; border: none;
width: 90%; width: 100%;
height: 95%; height: 100%;
max-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 { .bottom-link {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
.popup-tabs { .popup-content-inner,
margin-top: .3rem; .tab-content-settings,
.popup-tabs-child,
#popup-header-contents {
padding-left: 0.7rem;
padding-right: 0.7rem;
} }
} }
@media screen and (max-width: 400px) { @media screen and (max-width: 400px) {

View file

@ -1,6 +1,8 @@
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isIOS = ua.match("iphone os"); const isIOS = ua.match("iphone os");
const isMobile = ua.match("android") || 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 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 regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
const notification = `<div class="notification-dot"></div>`; const notification = `<div class="notification-dot"></div>`;
@ -14,10 +16,11 @@ const switchers = {
"vimeoDash": ["false", "true"], "vimeoDash": ["false", "true"],
"audioMode": ["false", "true"] "audioMode": ["false", "true"]
}; };
const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"]; const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio", "reduceTransparency", "disableAnimations"];
const exceptions = { // used for mobile devices const exceptions = { // used for mobile devices
"vQuality": "720" "vQuality": "720"
}; };
const bottomPopups = ["error", "download"]
let store = {}; let store = {};
@ -156,16 +159,18 @@ function notificationCheck(type) {
function hideAllPopups() { function hideAllPopups() {
let filter = document.getElementsByClassName('popup'); let filter = document.getElementsByClassName('popup');
for (let i = 0; i < filter.length; i++) { for (let i = 0; i < filter.length; i++) {
filter[i].style.visibility = "hidden"; filter[i].classList.remove("visible");
} }
eid("picker-holder").innerHTML = ''; eid("picker-holder").innerHTML = '';
eid("picker-download").href = '/'; eid("picker-download").href = '/';
eid("picker-download").style.visibility = "hidden"; eid("picker-download").classList.remove("visible");
eid("popup-backdrop").style.visibility = "hidden"; eid("popup-backdrop").classList.remove("visible");
store.isPopupOpen = false;
} }
function popup(type, action, text) { function popup(type, action, text) {
if (action === 1) { if (action === 1) {
hideAllPopups(); // hide the previous popup before showing a new one hideAllPopups(); // hide the previous popup before showing a new one
store.isPopupOpen = true;
switch (type) { switch (type) {
case "about": case "about":
let tabId = sGet("seenAbout") ? "changelog" : "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("popup-picker").classList.contains("scrollable")) eid("popup-picker").classList.add("scrollable");
if (eid("picker-holder").classList.contains("various")) eid("picker-holder").classList.remove("various"); if (eid("picker-holder").classList.contains("various")) eid("picker-holder").classList.remove("various");
eid("picker-download").href = text.audio; eid("picker-download").href = text.audio;
eid("picker-download").style.visibility = "visible" eid("picker-download").classList.add("visible");
for (let i in text.arr) { for (let i in text.arr) {
eid("picker-holder").innerHTML += `<a class="picker-image-container"><img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'"></img></a>` eid("picker-holder").innerHTML += `<a class="picker-image-container"><img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'"></img></a>`
} }
@ -206,12 +211,12 @@ function popup(type, action, text) {
let s = text.arr[i], item; let s = text.arr[i], item;
switch (s.type) { switch (s.type) {
case "video": case "video":
item = `<a class="picker-various-container" href="${text.arr[i]["url"]}" target="_blank"><div class="picker-element-name">VIDEO ${Number(i)+1}</div><div class="imageBlock"></div><img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'"></img></a>` item = `<div class="picker-various-container" onClick="${isIOS ? `share('${text.arr[i]["url"]}')` : `window.location.href='${text.arr[i]["url"]}'`}"><div class="picker-element-name">VIDEO ${Number(i)+1}</div><div class="imageBlock"></div><img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'"></img></div>`
break; break;
} }
eid("picker-holder").innerHTML += item eid("picker-holder").innerHTML += item
} }
eid("picker-download").style.visibility = "hidden"; eid("picker-download").classList.remove("visible");
break; break;
} }
break; break;
@ -219,14 +224,17 @@ function popup(type, action, text) {
break; break;
} }
} else { } else {
store.isPopupOpen = false;
if (type === "picker") { if (type === "picker") {
eid("picker-download").href = '/'; eid("picker-download").href = '/';
eid("picker-download").style.visibility = "hidden" eid("picker-download").classList.remove("visible");
eid("picker-holder").innerHTML = '' eid("picker-holder").innerHTML = ''
} }
} }
eid("popup-backdrop").style.visibility = vis(action); if (bottomPopups.includes(type)) eid(`popup-${type}-container`).classList.toggle("visible");
eid(`popup-${type}`).style.visibility = vis(action); eid("popup-backdrop").classList.toggle("visible");
eid(`popup-${type}`).classList.toggle("visible");
eid(`popup-${type}`).focus();
} }
function changeSwitcher(li, b) { function changeSwitcher(li, b) {
if (b) { if (b) {
@ -249,15 +257,12 @@ function checkbox(action) {
sSet(action, !!eid(action).checked); sSet(action, !!eid(action).checked);
switch(action) { switch(action) {
case "alwaysVisibleButton": button(); break; 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(); action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
} }
function loadSettings() { function loadSettings() {
try {
if (typeof(navigator.clipboard.readText) == "undefined") throw new Error();
} catch (err) {
eid("paste").style.display = "none";
}
if (sGet("alwaysVisibleButton") === "true") { if (sGet("alwaysVisibleButton") === "true") {
eid("alwaysVisibleButton").checked = true; eid("alwaysVisibleButton").checked = true;
eid("download-button").value = '>>' eid("download-button").value = '>>'
@ -266,6 +271,12 @@ function loadSettings() {
if (sGet("downloadPopup") === "true" && !isIOS) { if (sGet("downloadPopup") === "true" && !isIOS) {
eid("downloadPopup").checked = true; 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++) { for (let i = 0; i < checkboxes.length; i++) {
if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true; if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true;
} }
@ -312,7 +323,17 @@ async function pasteClipboard() {
eid("url-input-area").value = t; eid("url-input-area").value = t;
download(eid("url-input-area").value); 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) { async function download(url) {
changeDownloadButton(2, '...'); changeDownloadButton(2, '...');
@ -409,7 +430,7 @@ async function download(url) {
async function loadCelebrationsEmoji() { async function loadCelebrationsEmoji() {
let bac = eid("about-footer").innerHTML; let bac = eid("about-footer").innerHTML;
try { 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) { if (j && j.status === "success" && j.text) {
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('<img class="emoji" draggable="false" height="22" width="22" alt="🐲" src="emoji/dragon_face.svg">', j.text); eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('<img class="emoji" draggable="false" height="22" width="22" alt="🐲" src="emoji/dragon_face.svg">', j.text);
} }
@ -426,7 +447,7 @@ async function loadOnDemand(elementId, blockId) {
if (store.historyContent) { if (store.historyContent) {
j = store.historyContent; j = store.historyContent;
} else { } else {
await fetch(`${apiURL}/api/onDemand?blockId=${blockId}`).then(async(r) => { await fetch(`/onDemand?blockId=${blockId}`).then(async(r) => {
j = await r.json(); j = await r.json();
if (j && j.status === "success") { if (j && j.status === "success") {
store.historyContent = j; store.historyContent = j;
@ -461,14 +482,28 @@ window.onload = () => {
button(); button();
} }
} }
eid("url-input-area").addEventListener("keydown", (event) => { eid("url-input-area").addEventListener("keydown", (e) => {
if (event.key === 'Escape') eid("url-input-area").value = '';
button(); button();
}) })
eid("url-input-area").addEventListener("keyup", (event) => { eid("url-input-area").addEventListener("keyup", (e) => {
if (event.key === 'Enter') eid("download-button").click(); if (e.key === 'Enter') eid("download-button").click();
}) })
document.onkeydown = (event) => { document.onkeydown = (e) => {
if (event.key === "Tab" || event.ctrlKey) eid("url-input-area").focus(); if (!store.isPopupOpen) {
if (event.key === 'Escape') hideAllPopups(); 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();
}
} }

View file

@ -4,21 +4,21 @@
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">create an issue on github</a>" "ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">create an issue on github</a>"
}, },
"strings": { "strings": {
"AppTitleCobalt": "cobalt",
"LinkInput": "paste the link here", "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.", "EmbedBriefDescription": "save what you love without ads, trackers, or other creepy bullshit.",
"MadeWithLove": "made with <3 by wukko", "MadeWithLove": "made with <3 by wukko",
"AccessibilityInputArea": "link input area", "AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup", "AccessibilityOpenAbout": "open about popup",
"AccessibilityDownloadButton": "download button", "AccessibilityDownloadButton": "download button",
"AccessibilityOpenSettings": "open settings popup", "AccessibilityOpenSettings": "open settings popup",
"AccessibilityClosePopup": "close the popup",
"AccessibilityOpenDonate": "open donation popup", "AccessibilityOpenDonate": "open donation popup",
"TitlePopupAbout": "what's {appName}?", "TitlePopupAbout": "what's cobalt?",
"TitlePopupSettings": "settings", "TitlePopupSettings": "settings",
"TitlePopupError": "uh-oh...", "TitlePopupError": "uh-oh...",
"TitlePopupChangelog": "what's new?", "TitlePopupChangelog": "what's new?",
"TitlePopupDonate": "support {appName}", "TitlePopupDonate": "support cobalt",
"TitlePopupDownload": "how to save?", "TitlePopupDownload": "how to save?",
"ErrorSomethingWentWrong": "something went wrong and i couldn't get anything for you. try again, but if issue persists, {ContactLink}.", "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?", "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.", "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!", "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.", "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.", "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 {appName} got blocked. try again, but if error persists, {ContactLink}.", "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!", "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!", "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", "SettingsAppearanceSubtitle": "appearance",
@ -46,7 +46,7 @@
"AccessibilityEnableDownloadPopup": "ask what to do with downloads", "AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.", "SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
"LinkGitHubChanges": "&gt;&gt; see previous commits and contribute on github", "LinkGitHubChanges": "&gt;&gt; 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 <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\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\".", "DownloadPopupDescriptionIOS": "easiest way to save videos on ios:\n1. add <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">this siri shortcut</a>.\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.", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"ClickToCopy": "press to copy", "ClickToCopy": "press to copy",
@ -87,13 +87,12 @@
"MediaPickerTitle": "pick what to save", "MediaPickerTitle": "pick what to save",
"MediaPickerExplanationPC": "click or right click to download what you want.", "MediaPickerExplanationPC": "click or right click to download what you want.",
"MediaPickerExplanationPhone": "press or press and hold 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!", "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}.", "ErrorCantProcess": "i couldn't process your request :(\nyou can try again, but if issue persists, please {ContactLink}.",
"ChangelogPressToHide": "collapse", "ChangelogPressToHide": "collapse",
"Donate": "donate", "Donate": "donate",
"DonateSub": "help it stay online", "DonateSub": "help it stay online",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, meaning that <span class=\"text-backdrop\">it's completely free to use</span>. 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\n<span class=\"text-backdrop\">your help is more appreciated than ever!</span>", "DonateExplanation": "cobalt does not (and will never) serve ads or sell your data, meaning that <span class=\"text-backdrop\">it's completely free to use</span>. 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\n<span class=\"text-backdrop\">your help is more appreciated than ever!</span>",
"DonateVia": "donate via", "DonateVia": "donate via",
"DonateHireMe": "...or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a> :)", "DonateHireMe": "...or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a> :)",
"SettingsVideoMute": "mute audio", "SettingsVideoMute": "mute audio",
@ -103,24 +102,32 @@
"CollapseSupport": "support & source code", "CollapseSupport": "support & source code",
"CollapsePrivacy": "privacy policy", "CollapsePrivacy": "privacy policy",
"ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!", "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:", "FollowSupport": "keep in touch with cobalt 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.", "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:", "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 <span class=\"text-backdrop\">20 seconds</span>. 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 <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> 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 <span class=\"text-backdrop\">20 seconds</span>. 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 <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">github repo</a> yourself and see that everything is as stated.",
"ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!", "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.", "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", "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.", "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", "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", "SettingsDubDefault": "original",
"SettingsDubAuto": "auto", "SettingsDubAuto": "auto",
"SettingsVimeoPrefer": "vimeo downloads type", "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", "ShareURL": "share",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!", "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.", "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!", "UrgentDonate": "cobalt needs your help!",
"PopupCloseDone": "done" "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 <a class=\"text-backdrop italic\" href=\"{repo}/wiki/Troubleshooting#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">here!</a>\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 <a class=\"text-backdrop italic\" href=\"{repo}/wiki/Troubleshooting\" target=\"_blank\">self-troubleshooting guide</a> first!",
"AccessibilityGoBack": "go back and close the popup"
} }
} }

View file

@ -4,21 +4,21 @@
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">напиши об этом на github (можно на русском)</a>" "ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">напиши об этом на github (можно на русском)</a>"
}, },
"strings": { "strings": {
"AppTitleCobalt": "кобальт",
"LinkInput": "вставь ссылку сюда", "LinkInput": "вставь ссылку сюда",
"AboutSummary": "{appName} - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.", "AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.", "EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.",
"MadeWithLove": "сделано wukko, с <3", "MadeWithLove": "сделано wukko, с <3",
"AccessibilityInputArea": "зона вставки ссылки", "AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с инфой", "AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания", "AccessibilityDownloadButton": "кнопка скачивания",
"AccessibilityOpenSettings": "открыть настройки", "AccessibilityOpenSettings": "открыть настройки",
"AccessibilityClosePopup": "закрыть окно",
"AccessibilityOpenDonate": "сделать пожертвование", "AccessibilityOpenDonate": "сделать пожертвование",
"TitlePopupAbout": "что за {appName}?", "TitlePopupAbout": "что за кобальт?",
"TitlePopupSettings": "настройки", "TitlePopupSettings": "настройки",
"TitlePopupError": "опаньки...", "TitlePopupError": "опаньки...",
"TitlePopupChangelog": "что нового?", "TitlePopupChangelog": "что нового?",
"TitlePopupDonate": "поддержи {appName}", "TitlePopupDonate": "поддержи кобальт",
"TitlePopupDownload": "как сохранить?", "TitlePopupDownload": "как сохранить?",
"ErrorSomethingWentWrong": "что-то пошло совсем не так и у меня не получилось ничего для тебя достать. попробуй ещё раз, но если так и не получится, {ContactLink}.", "ErrorSomethingWentWrong": "что-то пошло совсем не так и у меня не получилось ничего для тебя достать. попробуй ещё раз, но если так и не получится, {ContactLink}.",
"ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?", "ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?",
@ -30,7 +30,7 @@
"ErrorLengthLimit": "я не могу обрабатывать видео длиннее чем {s} минут(ы), так что скачай что-нибудь покороче!", "ErrorLengthLimit": "я не могу обрабатывать видео длиннее чем {s} минут(ы), так что скачай что-нибудь покороче!",
"ErrorBadFetch": "произошла какая-то ошибка при получении данных по твоей ссылке. убедись, что она работает, и попробуй ещё раз.", "ErrorBadFetch": "произошла какая-то ошибка при получении данных по твоей ссылке. убедись, что она работает, и попробуй ещё раз.",
"ErrorNoInternet": "не получилось подключиться к серверу. проверь подключение к интернету и попробуй ещё раз!", "ErrorNoInternet": "не получилось подключиться к серверу. проверь подключение к интернету и попробуй ещё раз!",
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же {appName} заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.", "ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же кобальт заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.",
"ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!", "ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!",
"ErrorLiveVideo": "я пока что не умею заглядывать в будущее, поэтому дождись окончания прямого эфира, и потом уже скачивай видео!", "ErrorLiveVideo": "я пока что не умею заглядывать в будущее, поэтому дождись окончания прямого эфира, и потом уже скачивай видео!",
"SettingsAppearanceSubtitle": "внешний вид", "SettingsAppearanceSubtitle": "внешний вид",
@ -46,13 +46,13 @@
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на github", "LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на github",
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.", "NoScriptMessage": "кобальт использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких зловредных скриптов, обещаю.",
"DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".", "DownloadPopupDescriptionIOS": "наиболее простой метод скачивания видео на ios:\n1. добавь <a class=\"text-backdrop italic\" href=\"{saveToGalleryShortcut}\" target=\"_blank\">этот сценарий siri</a>.\n2. нажми \"поделиться\" выше и выбери \"save to photos\" в открывшемся окне.\nесли появляется окно с запросом разрешения, то прочитай его, потом нажми \"всегда разрешать\".\n\nальтернативный метод:\nзажми кнопку \"скачать\", затем скрой превью и выбери \"загрузить файл по ссылке\" в появившемся окне.\nпотом открой загрузки в safari, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
"ClickToCopy": "нажми, чтобы скопировать", "ClickToCopy": "нажми, чтобы скопировать",
"Download": "скачать", "Download": "скачать",
"CopyURL": "скопировать", "CopyURL": "скопировать",
"AboutTab": "о {appName}", "AboutTab": "о кобальте",
"ChangelogTab": "изменения", "ChangelogTab": "изменения",
"DonationsTab": "донаты", "DonationsTab": "донаты",
"SettingsVideoTab": "видео", "SettingsVideoTab": "видео",
@ -93,7 +93,7 @@
"ChangelogPressToHide": "скрыть", "ChangelogPressToHide": "скрыть",
"Donate": "задонатить", "Donate": "задонатить",
"DonateSub": "ты можешь помочь!", "DonateSub": "ты можешь помочь!",
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что разработка и поддержка сервиса, которым пользуются более 300 тысяч людей, обходится довольно затратно.\n\nесли {appName} тебе помог и ты хочешь, чтобы он продолжал работать, то это можно сделать через донаты!\n\nиспользование {appName} по всему миру растёт с каждым днём, а в след за ним и стоимость хостинга. мне, как первокурснику, оплачивать такое в одиночку довольно трудно.\n\nя еще ничего не заработал на {appName}, всё возвращается обратно пользователям, так что ты помогаешь всем, кто использует {appName}.\n\n<span class=\"text-backdrop\">твой донат на вес золота, ценится как никогда!</span>", "DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что разработка и поддержка сервиса, которым пользуются более 300 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал работать, то это можно сделать через донаты!\n\nиспользование кобальта по всему миру растёт с каждым днём, а в след за ним и стоимость хостинга. мне, как первокурснику, оплачивать такое в одиночку довольно трудно.\n\nя еще ничего не заработал на кобальте, всё возвращается обратно пользователям, так что ты помогаешь всем, кто использует кобальт.\n\n<span class=\"text-backdrop\">твой донат на вес золота, ценится как никогда!</span>",
"DonateVia": "открыть", "DonateVia": "открыть",
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)", "DonateHireMe": "...или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
"SettingsVideoMute": "убрать аудио", "SettingsVideoMute": "убрать аудио",
@ -103,24 +103,32 @@
"CollapseSupport": "поддержка и исходный код", "CollapseSupport": "поддержка и исходный код",
"CollapsePrivacy": "политика конфиденциальности", "CollapsePrivacy": "политика конфиденциальности",
"ServicesNote": "этот список далеко не финальный и постоянно пополняется. заглядывай сюда почаще, тогда точно будешь знать, что поддерживается!", "ServicesNote": "этот список далеко не финальный и постоянно пополняется. заглядывай сюда почаще, тогда точно будешь знать, что поддерживается!",
"FollowSupport": "оставайтесь на связи с {appName} для новостей, поддержки, участия в опросах, и многого другого:", "FollowSupport": "оставайтесь на связи с кобальтом для новостей, поддержки, участия в опросах, и многого другого:",
"SupportNote": "так как я один занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.", "SupportNote": "так как я один занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.",
"SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:",
"PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - только твоё личное дело.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы такого типа загрузок.\n\nв этом случае данные о запрошенном стриме хранятся в ОЗУ сервера в течение <span class=\"text-backdrop\">20 секунд</span>. по истечении этого периода всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальный код {appName} не предоставляет такой возможности.\n\nты всегда можешь посмотреть <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">исходный код {appName}</a> и убедиться, что всё так, как описано.", "PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - только твоё личное дело.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы такого типа загрузок.\n\nв этом случае данные о запрошенном стриме хранятся в ОЗУ сервера в течение <span class=\"text-backdrop\">20 секунд</span>. по истечении этого периода всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальный код кобальта не предоставляет такой возможности.\n\nты всегда можешь посмотреть <a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">исходный код кобальт</a> и убедиться, что всё так, как описано.",
"ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!", "ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!",
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!", "ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
"SettingsCodecSubtitle": "кодек для видео с youtube", "SettingsCodecSubtitle": "кодек для видео с youtube",
"SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.", "SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.",
"SettingsAudioDub": "звуковая дорожка для видео с youtube", "SettingsAudioDub": "звуковая дорожка для видео с youtube",
"SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера (и {appName}).", "SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.",
"SettingsDubDefault": "оригинал", "SettingsDubDefault": "оригинал",
"SettingsDubAuto": "авто", "SettingsDubAuto": "авто",
"SettingsVimeoPrefer": "тип загрузок с vimeo", "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": "поделиться", "ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
"UrgentDonate": "нужна твоя помощь!", "UrgentDonate": "нужна твоя помощь!",
"PopupCloseDone": "готово" "PopupCloseDone": "готово",
"Accessibility": "общедоступность",
"SettingsReduceTransparency": "уменьшить прозрачность",
"SettingsDisableAnimations": "выключить анимации",
"FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!",
"ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным <a class=\"text-backdrop italic\" href=\"{repo}/wiki/Troubleshooting#how-to-fix-clipboard-pasting-in-firefox\" target=\"_blank\">здесь</a>\n\n...или же ты можешь просто вставить ссылку вручную.",
"ClipboardErrorNoPermission": "кобальт не может прочитать последний элемент в буфере обмена без твоего разрешения.\n\nесли ты не хочешь давать доступ, просто вставь ссылку вручную.\n\nну а если хочешь, то открой настройки сайта и разреши доступ на чтение буфера обмена.",
"SupportSelfTroubleshooting": "возникли проблемы? попробуй сначала исправить всё сам <a class=\"text-backdrop italic\" href=\"{repo}/wiki/Troubleshooting\" target=\"_blank\">по этому гиду!</a>",
"AccessibilityGoBack": "вернуться назад и закрыть окно"
} }
} }

View file

@ -1,5 +1,5 @@
import * as fs from "fs"; 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"; import loadJson from "../modules/sub/loadJSON.js";
const locPath = './src/localization/languages'; const locPath = './src/localization/languages';
@ -16,7 +16,7 @@ export async function loadLoc() {
} }
export function replaceBase(s) { export function replaceBase(s) {
return s.replace(/\n/g, '<br/>').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{appName}/g, appName).replace(/{repo}/g, repo).replace(/\*;/g, "&bull;"); return s.replace(/\n/g, '<br/>').replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut).replace(/{repo}/g, repo).replace(/\*;/g, "&bull;");
} }
export function replaceAll(lang, str, string, replacement) { export function replaceAll(lang, str, string, replacement) {
let s = replaceBase(str[string]) let s = replaceBase(str[string])

View file

@ -1,14 +1,10 @@
import * as esbuild from "esbuild"; import * as esbuild from "esbuild";
import * as fs from "fs"; import * as fs from "fs";
import { loadLoc, languageList } from "../localization/manager.js"; import { loadLoc, languageList } from "../localization/manager.js";
import { cleanHTML } from "./sub/utils.js";
import page from "./pageRender/page.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) { export async function buildFront(commitHash, branch) {
try { try {
// preload localization files // preload localization files

View file

@ -1,11 +1,18 @@
{ {
"current": { "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", "version": "6.2",
"date": "June 27 2023",
"title": "all network issues have been fixed!", "title": "all network issues have been fixed!",
"banner": "meowthhammer.webp", "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! <span class='text*;backdrop'>there should no longer be any networking issues</span>.\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" "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! <span class='text*;backdrop'>there should no longer be any networking issues</span>.\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", "version": "6.0",
"title": "better reliability, new infrastructure, pinterest support, and way more!", "title": "better reliability, new infrastructure, pinterest support, and way more!",
"banner": "catswitchboxes.webp", "banner": "catswitchboxes.webp",

View file

@ -6,8 +6,12 @@ let changelog = loadJSON('./src/modules/changelog/changelog.json')
export default function(string) { export default function(string) {
try { try {
switch (string) { switch (string) {
case "version":
return `<span class="text-backdrop changelog-tag-version">v.${changelog["current"]["version"]}</span>${
changelog["current"]["date"] ? `<span class="changelog-tag-date">· ${changelog["current"]["date"]}</span>` : ''
}`
case "title": case "title":
return `<span class="text-backdrop">${changelog["current"]["version"]}:</span> ${replaceBase(changelog["current"]["title"])}`; return replaceBase(changelog["current"]["title"]);
case "banner": case "banner":
return changelog["current"]["banner"] ? `updateBanners/${changelog["current"]["banner"]}` : false; return changelog["current"]["banner"] ? `updateBanners/${changelog["current"]["banner"]}` : false;
case "content": case "content":
@ -15,9 +19,11 @@ export default function(string) {
case "history": case "history":
return changelog["history"].map((i) => { return changelog["history"].map((i) => {
return { return {
title: `<span class="text-backdrop">${i["version"]}:</span> ${replaceBase(i["title"])}`, title: replaceBase(i["title"]),
version: `<span class="text-backdrop changelog-tag-version">v.${i["version"]}</span>${
i["date"] ? `<span class="changelog-tag-date">· ${i["date"]}</span>` : ''
}`,
content: replaceBase(i["content"]), content: replaceBase(i["content"]),
version: i["version"],
banner: i["banner"] ? `updateBanners/${i["banner"]}` : false, banner: i["banner"] ? `updateBanners/${i["banner"]}` : false,
} }
}); });

View file

@ -6,7 +6,6 @@ const servicesConfigJson = loadJson("./src/modules/processing/servicesConfig.jso
export const export const
services = servicesConfigJson.config, services = servicesConfigJson.config,
audioIgnore = servicesConfigJson.audioIgnore, audioIgnore = servicesConfigJson.audioIgnore,
appName = packageJson.name,
version = packageJson.version, version = packageJson.version,
streamLifespan = config.streamLifespan, streamLifespan = config.streamLifespan,
maxVideoDuration = config.maxVideoDuration, maxVideoDuration = config.maxVideoDuration,

View file

@ -1,6 +1,10 @@
import { celebrations } from "../config.js"; import { celebrations } from "../config.js";
import emoji from "../emoji.js"; import emoji from "../emoji.js";
export const backButtonSVG = `<svg width="22" height="22" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.7551 27.5102L3 15.7551L14.7551 4L16.7755 5.99417L8.45773 14.312H30V17.1982H8.45773L16.7755 25.4898L14.7551 27.5102Z" fill="#FFFFFF"/>
</svg>`
export function switcher(obj) { export function switcher(obj) {
let items = ``; let items = ``;
if (obj.name === "download") { if (obj.name === "download") {
@ -19,27 +23,19 @@ export function switcher(obj) {
${obj.explanation ? `<div class="explanation">${obj.explanation}</div>` : ``} ${obj.explanation ? `<div class="explanation">${obj.explanation}</div>` : ``}
</div>` </div>`
} }
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) { checkboxes += `<label id="${obj[i].action}-chkbx" class="checkbox${paddingClass}">
let paddingClass = ` ` <input id="${obj[i].action}" type="checkbox" aria-label="${obj[i].aria ? obj[i].aria : obj[i].name}" onclick="checkbox('${obj[i].action}')">
switch (paddingType) { <span>${obj[i].name}</span>
case 1:
paddingClass += "bottom-margin"
break;
case 2:
paddingClass += "top-margin"
break;
case 3:
paddingClass += "no-margin"
break;
case 4:
paddingClass += "top-margin-only"
}
return `<label id="${action}-chkbx" class="checkbox${paddingClass}">
<input id="${action}" type="checkbox" ${aria ? `aria-label="${aria}"` : `aria-label="${text}"`} onclick="checkbox('${action}')">
<span>${text}</span>
</label>` </label>`
} }
return checkboxes
}
export function sep(paddingType) { export function sep(paddingType) {
let paddingClass = `` let paddingClass = ``
switch(paddingType) { switch(paddingType) {
@ -50,7 +46,7 @@ export function sep(paddingType) {
return `<div class="separator${paddingClass}"></div>` return `<div class="separator${paddingClass}"></div>`
} }
export function popup(obj) { export function popup(obj) {
let classes = obj.classes ? obj.classes : [] let classes = obj.classes ? obj.classes : [];
let body = obj.body; let body = obj.body;
if (Array.isArray(obj.body)) { if (Array.isArray(obj.body)) {
body = `` body = ``
@ -65,37 +61,44 @@ export function popup(obj) {
} }
} }
return ` return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center box${classes.length > 0 ? ' ' + classes.join(' ') : ''}" style="visibility: hidden;">` : ''} ${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box": ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''}
<div id="popup-header" class="popup-header"> <div id="popup-header" class="popup-header${!obj.buttonOnly ? " glass-bkg": ''}">
${obj.standalone && !obj.buttonOnly ? `<button id="close-button" class="switch up" onclick="popup('${obj.name}', 0)" ${obj.header.closeAria ? `aria-label="${obj.header.closeAria}"` : ''}>x</button>` : ''} <div id="popup-header-contents">
${obj.buttonOnly ? obj.header.emoji : ``} ${obj.buttonOnly ? obj.header.emoji : ``}
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''} ${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div> </div>
<div id="popup-content"${obj.footer ? ' class="with-footer"' : ''}> </div>
<div id="popup-content" class="popup-content-inner">
${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''} ${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
</div> </div>
${obj.footer ? `<div id="popup-footer" class="popup-footer">
<a id="popup-bottom" class="popup-footer-content" target="_blank" href="${obj.footer.url}">${obj.footer.text}</a>
</div>` : ''}
${obj.standalone ? `</div>` : ''}` ${obj.standalone ? `</div>` : ''}`
} }
export function multiPagePopup(obj) { export function multiPagePopup(obj) {
let tabs = `` let tabs = `
let tabContent = `` <button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
${backButtonSVG}
</button>`;
let tabContent = ``;
for (let i = 0; i < obj.tabs.length; i++) { for (let i = 0; i < obj.tabs.length; i++) {
tabs += `<button id="tab-button-${obj.name}-${obj.tabs[i]["name"]}" class="switch tab tab-${obj.name}" onclick="changeTab(event, 'tab-${obj.name}-${obj.tabs[i]["name"]}', '${obj.name}')">${obj.tabs[i]["title"]}</button>` tabs += `<button id="tab-button-${obj.name}-${obj.tabs[i]["name"]}" class="switch tab tab-${obj.name}" onclick="changeTab(event, 'tab-${obj.name}-${obj.tabs[i]["name"]}', '${obj.name}')">${obj.tabs[i]["title"]}</button>`
tabContent += `<div id="tab-${obj.name}-${obj.tabs[i]["name"]}" class="popup-tab-content tab-content-${obj.name}">${obj.tabs[i]["content"]}</div>` tabContent += `<div id="tab-${obj.name}-${obj.tabs[i]["name"]}" class="popup-tab-content tab-content-${obj.name}">${obj.tabs[i]["content"]}</div>`
} }
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;"> <div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header"> <div id="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header glass-bkg">
<div id="popup-header-contents">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''} ${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}</div>` : ''}${tabContent}</div> ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
<div id="popup-tabs" class="switches popup-tabs"><div class="switches popup-tabs-child">${tabs}</div><button id="close-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button></div> </div>
</div>` : ''}${tabContent}</div>
<div id="popup-tabs" class="switches popup-tabs glass-bkg"><div class="switches popup-tabs-child">${tabs}</div></div>
</div>` </div>`
} }
export function collapsibleList(arr) { export function collapsibleList(arr) {
@ -112,24 +115,28 @@ export function collapsibleList(arr) {
return items; return items;
} }
export function popupWithBottomButtons(obj) { export function popupWithBottomButtons(obj) {
let tabs = `` let tabs = `
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
${backButtonSVG}
</button>`
for (let i = 0; i < obj.buttons.length; i++) { for (let i = 0; i < obj.buttons.length; i++) {
tabs += obj.buttons[i] tabs += obj.buttons[i]
} }
tabs += `<button id="close-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;"> <div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header"> <div id="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header glass-bkg">
<div id="popup-header-contents">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''} ${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}</div>` : ''}${obj.content}</div> ${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}
<div id="popup-buttons" class="switches popup-tabs">${tabs}</div> </div>
</div>` : ''}${obj.content}</div>
<div id="popup-tabs" class="switches popup-tabs glass-bkg"><div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div></div>
</div>` </div>`
} }
export function backdropLink(link, text) {
return `<a class="text-backdrop italic" href="${link}" target="_blank">${text}</a>`
}
export function socialLink(emji, name, handle, url) { export function socialLink(emji, name, handle, url) {
return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop italic" href="${url}" target="_blank">${handle}</a></div>` return `<div class="cobalt-support-link">${emji} ${name}: <a class="text-backdrop italic" href="${url}" target="_blank">${handle}</a></div>`
} }

View file

@ -1,4 +1,5 @@
import changelogManager from "../changelog/changelogManager.js" import changelogManager from "../changelog/changelogManager.js"
import { cleanHTML } from "../sub/utils.js";
let cache = {} let cache = {}
@ -10,8 +11,16 @@ export function changelogHistory() { // blockId 0
let historyLen = history.length; let historyLen = history.length;
for (let i in history) { for (let i in history) {
let separator = (i !== 0 && i !== historyLen) ? '<div class="separator"></div>' : ''; let separator = (i !== 0 && i !== historyLen) ? '<div class="separator"></div>' : '';
render += `${separator}${history[i]["banner"] ? `<div class="changelog-banner"><img class="changelog-img" src="${history[i]["banner"]}" onerror="this.style.display='none'" loading="lazy"></img></div>` : ''}<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div><div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
render += `
${separator}${history[i]["banner"] ? `<div class="changelog-banner">
<img class="changelog-img" src="${history[i]["banner"]}" onerror="this.style.display='none'" loading="lazy"></img>
</div>` : ''}
<div id="popup-desc" class="changelog-tags">${history[i]["version"]}</div>
<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div>
<div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
} }
render = cleanHTML(render);
cache['0'] = render; cache['0'] = render;
return render; return render;
} }

View file

@ -1,5 +1,5 @@
import { backdropLink, checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js"; import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js";
import { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js"; import { services as s, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js"; import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import emoji from "../emoji.js"; import emoji from "../emoji.js";
@ -30,6 +30,7 @@ for (let i in donations["crypto"]) {
export default function(obj) { export default function(obj) {
const t = (str, replace) => { return loc(obj.lang, str, replace) }; const t = (str, replace) => { return loc(obj.lang, str, replace) };
let ua = obj.useragent.toLowerCase(); let ua = obj.useragent.toLowerCase();
let isIOS = ua.match("iphone os"); let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os"); let isMobile = ua.match("android") || ua.match("iphone os");
@ -40,26 +41,32 @@ export default function(obj) {
audioFormats[0]["text"] = t('SettingsAudioFormatBest'); audioFormats[0]["text"] = t('SettingsAudioFormatBest');
try { try {
return `<!DOCTYPE html> return `
<!DOCTYPE html>
<html lang="${obj.lang}"> <html lang="${obj.lang}">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="viewport-fit=cover width=device-width, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" /> <meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" />
<title>${appName}</title> <title>${t("AppTitleCobalt")}</title>
<meta property="og:url" content="${process.env.webURL || process.env.selfURL}" /> <meta property="og:url" content="${process.env.webURL || process.env.selfURL}" />
<meta property="og:title" content="${appName}" /> <meta property="og:title" content="${t("AppTitleCobalt")}" />
<meta property="og:description" content="${t('EmbedBriefDescription')}" /> <meta property="og:description" content="${t('EmbedBriefDescription')}" />
<meta property="og:image" content="${process.env.webURL || process.env.selfURL}icons/generic.png" /> <meta property="og:image" content="${process.env.webURL || process.env.selfURL}icons/generic.png" />
<meta name="title" content="${appName}" /> <meta name="title" content="${t("AppTitleCobalt")}" />
<meta name="description" content="${t('AboutSummary')}" /> <meta name="description" content="${t('AboutSummary')}" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="${t("AppTitleCobalt")}">
<link rel="icon" type="image/x-icon" href="icons/favicon.ico" /> <link rel="icon" type="image/x-icon" href="icons/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
<link rel="manifest" href="manifest.webmanifest" /> <link rel="manifest" href="manifest.webmanifest" />
@ -69,9 +76,10 @@ export default function(obj) {
<noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript> <noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript>
</head> </head>
<body id="cobalt-body" ${platform === "p" ? 'class="desktop"' : ''} data-nosnippet ontouchstart> <body id="cobalt-body" ${platform === "p" ? 'class="desktop"' : ''} data-nosnippet ontouchstart>
<body id="notification-area"></div>
${multiPagePopup({ ${multiPagePopup({
name: "about", name: "about",
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
tabs: [{ tabs: [{
name: "about", name: "about",
title: `${emoji("🐲")} ${t('AboutTab')}`, title: `${emoji("🐲")} ${t('AboutTab')}`,
@ -82,30 +90,40 @@ export default function(obj) {
text: t('MadeWithLove'), text: t('MadeWithLove'),
url: authorInfo.link url: authorInfo.link
}, },
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}` title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
}, },
body: [{ body: [{
text: t('AboutSummary') text: t('AboutSummary')
}, { }, {
text: collapsibleList([{ text: collapsibleList([{
"name": "services", name: "services",
"title": t("CollapseServices"), title: t("CollapseServices"),
"body": `${enabledServices}<br/><br/>${t("ServicesNote")}` body: `${enabledServices}<br/><br/>${t("ServicesNote")}`
}, { }, {
"name": "support", name: "support",
"title": t("CollapseSupport"), title: t("CollapseSupport"),
"body": `${t("FollowSupport")}<br/> body: `
${socialLink(emoji("🐦"), "twitter", authorInfo.support.twitter.handle, authorInfo.support.twitter.url)} ${t("SupportSelfTroubleshooting")}<br/>
${socialLink(emoji("👾"), "discord", authorInfo.support.discord.handle, authorInfo.support.discord.url)} ${t("FollowSupport")}<br/>
${socialLink(emoji("🐘"), "mastodon", authorInfo.support.mastodon.handle, authorInfo.support.mastodon.url)}<br/> ${socialLink(
emoji("🐦"), "twitter", authorInfo.support.twitter.handle, authorInfo.support.twitter.url
)}
${socialLink(
emoji("👾"), "discord", authorInfo.support.discord.handle, authorInfo.support.discord.url
)}
${socialLink(
emoji("🐘"), "mastodon", authorInfo.support.mastodon.handle, authorInfo.support.mastodon.url
)}<br/>
${t("SourceCode")}<br/> ${t("SourceCode")}<br/>
${socialLink(emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo)}<br/> ${socialLink(
emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo
)}<br/>
${t("SupportNote")}` ${t("SupportNote")}`
}, { }, {
"name": "privacy", name: "privacy",
"title": t("CollapsePrivacy"), title: t("CollapsePrivacy"),
"body": t("PrivacyPolicy") body: t("PrivacyPolicy")
}]) }])
}] }]
}) })
@ -115,7 +133,7 @@ export default function(obj) {
content: popup({ content: popup({
name: "changelog", name: "changelog",
header: { header: {
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
title: `${emoji("🪄", 30)} ${t('TitlePopupChangelog')}` title: `${emoji("🪄", 30)} ${t('TitlePopupChangelog')}`
}, },
body: [{ body: [{
@ -123,8 +141,14 @@ export default function(obj) {
raw: true raw: true
}, { }, {
text: changelogManager("banner") ? text: changelogManager("banner") ?
`<div class="changelog-banner"><img class="changelog-img" src="${changelogManager("banner")}" onerror="this.style.display='none'" loading="lazy"></img></div>`: '', `<div class="changelog-banner">
<img class="changelog-img" src="${changelogManager("banner")}" onerror="this.style.display='none'" loading="lazy"></img>
</div>`: '',
raw: true raw: true
}, {
text: changelogManager("version"),
classes: ["changelog-tags"],
nopadding: true
}, { }, {
text: changelogManager("title"), text: changelogManager("title"),
classes: ["changelog-subtitle"], classes: ["changelog-subtitle"],
@ -132,19 +156,26 @@ export default function(obj) {
}, { }, {
text: changelogManager("content") text: changelogManager("content")
}, { }, {
text: `${sep()}<span class="text-backdrop">${obj.hash}:</span> ${com[0]}`, text: sep(),
raw: true
},{
text: `<a class="text-backdrop changelog-tag-version" href="${repo}/commit/${obj.hash}">#${obj.hash}</a>`,
classes: ["changelog-tags"],
nopadding: true
}, {
text: com[0],
classes: ["changelog-subtitle"], classes: ["changelog-subtitle"],
nopadding: true nopadding: true
}, { }, {
text: com[1] text: com[1]
}, {
text: backdropLink(`${repo}/commits`, t('LinkGitHubChanges')),
classes: ["bottom-link"]
}, { }, {
text: `<div class="category-title">${t('ChangelogOlder')}</div>`, text: `<div class="category-title">${t('ChangelogOlder')}</div>`,
raw: true raw: true
}, { }, {
text: `<div id="changelog-history"><button class="switch bottom-margin" onclick="loadOnDemand('changelog-history', '0')">${t("ChangelogPressToExpand")}</button></div>`, text: `
<div id="changelog-history">
<button class="switch bottom-margin" onclick="loadOnDemand('changelog-history', '0')">${t("ChangelogPressToExpand")}</button>
</div>`,
raw: true raw: true
}] }]
}) })
@ -154,14 +185,17 @@ export default function(obj) {
content: popup({ content: popup({
name: "donate", name: "donate",
header: { header: {
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
title: emoji("💸", 30) + t('TitlePopupDonate') title: emoji("💸", 30) + t('TitlePopupDonate')
}, },
body: [{ body: [{
text: `<div class="category-title">${t('DonateSub')}</div>`, text: `<div class="category-title">${t('DonateSub')}</div>`,
raw: true raw: true
}, { }, {
text: `<div class="changelog-banner"><img class="changelog-img" src="updateBanners/catsleep.webp" onerror="this.style.display='none'" loading="lazy"></img></div>`, text: `
<div class="changelog-banner">
<img class="changelog-img" src="updateBanners/catsleep.webp" onerror="this.style.display='none'" loading="lazy"></img>
</div>`,
raw: true raw: true
}, { }, {
text: t('DonateExplanation') text: t('DonateExplanation')
@ -189,7 +223,7 @@ export default function(obj) {
})} })}
${multiPagePopup({ ${multiPagePopup({
name: "settings", name: "settings",
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
header: { header: {
aboveTitle: { aboveTitle: {
text: `v.${version}-${obj.hash}${platform} (${obj.branch})`, text: `v.${version}-${obj.hash}${platform} (${obj.branch})`,
@ -207,33 +241,37 @@ export default function(obj) {
name: "vQuality", name: "vQuality",
explanation: t('SettingsQualityDescription'), explanation: t('SettingsQualityDescription'),
items: [{ items: [{
"action": "max", action: "max",
"text": "4320p+" text: "8k+"
}, { }, {
"action": "2160", action: "2160",
"text": "2160p" text: "4k"
}, { }, {
"action": "1440", action: "1440",
"text": "1440p" text: "1440p"
}, { }, {
"action": "1080", action: "1080",
"text": "1080p" text: "1080p"
}, { }, {
"action": "720", action: "720",
"text": "720p" text: "720p"
}, { }, {
"action": "480", action: "480",
"text": "480p" text: "480p"
}, { }, {
"action": "360", action: "360",
"text": "360p" text: "360p"
}] }]
}) })
}) })
+ settingsCategory({ + settingsCategory({
name: "tiktok", name: "tiktok",
title: "tiktok", title: "tiktok",
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'), 3) body: checkbox([{
action: "disableTikTokWatermark",
name: t("SettingsRemoveWatermark"),
padding: "no-margin"
}])
}) })
+ settingsCategory({ + settingsCategory({
name: t('SettingsCodecSubtitle'), name: t('SettingsCodecSubtitle'),
@ -241,14 +279,14 @@ export default function(obj) {
name: "vCodec", name: "vCodec",
explanation: t('SettingsCodecDescription'), explanation: t('SettingsCodecDescription'),
items: [{ items: [{
"action": "h264", action: "h264",
"text": "h264 (mp4)" text: "h264 (mp4)"
}, { }, {
"action": "av1", action: "av1",
"text": "av1 (mp4)" text: "av1 (mp4)"
}, { }, {
"action": "vp9", action: "vp9",
"text": "vp9 (webm)" text: "vp9 (webm)"
}] }]
}) })
}) })
@ -258,11 +296,11 @@ export default function(obj) {
name: "vimeoDash", name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'), explanation: t('SettingsVimeoPreferDescription'),
items: [{ items: [{
"action": "false", action: "false",
"text": "progressive" text: "progressive"
}, { }, {
"action": "true", action: "true",
"text": "dash" text: "dash"
}] }]
}) })
}) })
@ -272,30 +310,43 @@ export default function(obj) {
content: settingsCategory({ content: settingsCategory({
name: "general", name: "general",
title: t('SettingsFormatSubtitle'), title: t('SettingsFormatSubtitle'),
body: body: switcher({
switcher({
name: "aFormat", name: "aFormat",
explanation: t('SettingsAudioFormatDescription'), explanation: t('SettingsAudioFormatDescription'),
items: audioFormats items: audioFormats
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation')) })
}) + settingsCategory({ + sep(0)
+ checkbox([{
action: "muteAudio",
name: t("SettingsVideoMute"),
padding: "no-margin"
}])
+ explanation(t('SettingsVideoMuteExplanation'))
})
+ settingsCategory({
name: "dub", name: "dub",
title: t("SettingsAudioDub"), title: t("SettingsAudioDub"),
body: switcher({ body: switcher({
name: "dubLang", name: "dubLang",
explanation: t('SettingsAudioDubDescription'), explanation: t('SettingsAudioDubDescription'),
items: [{ items: [{
"action": "original", action: "original",
"text": t('SettingsDubDefault') text: t('SettingsDubDefault')
}, { }, {
"action": "auto", action: "auto",
"text": t('SettingsDubAuto') text: t('SettingsDubAuto')
}] }]
}) })
}) + settingsCategory({ })
+ settingsCategory({
name: "tiktok", name: "tiktok",
title: "tiktok", title: "tiktok",
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + explanation(t('SettingsAudioFullTikTokDescription')) body: checkbox([{
action: "fullTikTokAudio",
name: t("SettingsAudioFullTikTok"),
padding: "no-margin"
}])
+ explanation(t('SettingsAudioFullTikTokDescription'))
}) })
}, { }, {
name: "other", name: "other",
@ -307,26 +358,51 @@ export default function(obj) {
name: "theme", name: "theme",
subtitle: t('SettingsThemeSubtitle'), subtitle: t('SettingsThemeSubtitle'),
items: [{ items: [{
"action": "auto", action: "auto",
"text": t('SettingsThemeAuto') text: t('SettingsThemeAuto')
}, { }, {
"action": "dark", action: "dark",
"text": t('SettingsThemeDark') text: t('SettingsThemeDark')
}, { }, {
"action": "light", action: "light",
"text": t('SettingsThemeLight') text: t('SettingsThemeLight')
}] }]
}) + checkbox("alwaysVisibleButton", t('SettingsKeepDownloadButton'), 4, t('AccessibilityKeepDownloadButton')) })
}) + settingsCategory({ })
+ settingsCategory({
name: "accessibility",
title: t('Accessibility'),
body: checkbox([{
action: "alwaysVisibleButton",
name: t("SettingsKeepDownloadButton"),
aria: t("AccessibilityKeepDownloadButton")
}, {
action: "reduceTransparency",
name: t("SettingsReduceTransparency")
}, {
action: "disableAnimations",
name: t("SettingsDisableAnimations"),
padding: "no-margin"
}])
})
+ settingsCategory({
name: "miscellaneous", name: "miscellaneous",
title: t('Miscellaneous'), title: t('Miscellaneous'),
body: checkbox("disableChangelog", t('SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", t('SettingsEnableDownloadPopup'), 1, t('AccessibilityEnableDownloadPopup')) : ''}` body: checkbox([{
action: "disableChangelog",
name: t("SettingsDisableNotifications")
}, {
action: "downloadPopup",
name: t("SettingsEnableDownloadPopup"),
padding: "no-margin",
aria: t("AccessibilityEnableDownloadPopup")
}])
}) })
}], }],
})} })}
${popupWithBottomButtons({ ${popupWithBottomButtons({
name: "picker", name: "picker",
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
header: { header: {
title: `<div id="picker-title"></div>`, title: `<div id="picker-title"></div>`,
explanation: `<div id="picker-subtitle"></div>`, explanation: `<div id="picker-subtitle"></div>`,
@ -334,42 +410,47 @@ export default function(obj) {
buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${t('ImagePickerDownloadAudio')}</a>`], buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${t('ImagePickerDownloadAudio')}</a>`],
content: '<div id="picker-holder"></div>' content: '<div id="picker-holder"></div>'
})} })}
<div id="popup-download-container" class="popup-from-bottom">
${popup({ ${popup({
name: "download", name: "download",
standalone: true, standalone: true,
buttonOnly: true, buttonOnly: true,
classes: ["small"], classes: ["small", "glass-bkg"],
header: { header: {
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
emoji: emoji("🐱", 78, 1, 1), emoji: emoji("🐱", 78, 1, 1),
title: t('TitlePopupDownload') title: t('TitlePopupDownload')
}, },
body: switcher({ body: switcher({
name: "download", name: "download",
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`, explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
items: `<a id="pd-download" class="switch full" target="_blank" href="/">${t('Download')}</a> items: `<a id="pd-download" class="switch full" target="_blank" href="/"><span>${t('Download')}</span></a>
<div id="pd-share" class="switch full">${t('ShareURL')}</div> <div id="pd-share" class="switch full">${t('ShareURL')}</div>
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>` <div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
}), }),
buttonText: t('PopupCloseDone') buttonText: t('PopupCloseDone')
})} })}
</div>
<div id="popup-error-container" class="popup-from-bottom">
${popup({ ${popup({
name: "error", name: "error",
standalone: true, standalone: true,
buttonOnly: true, buttonOnly: true,
classes: ["small"], classes: ["small", "glass-bkg"],
header: { header: {
closeAria: t('AccessibilityClosePopup'), closeAria: t('AccessibilityGoBack'),
title: t('TitlePopupError'), title: t('TitlePopupError'),
emoji: emoji("😿", 78, 1, 1), emoji: emoji("😿", 78, 1, 1),
}, },
body: `<div id="desc-error" class="desc-padding subtext"></div>`, body: `<div id="desc-error" class="desc-padding subtext"></div>`,
buttonText: t('ErrorPopupCloseButton') buttonText: t('ErrorPopupCloseButton')
})} })}
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div> </div>
<div id="urgent-notice" class="urgent-notice explanation center" onclick="popup('about', 1, 'donate')" style="visibility: hidden;">${emoji("💖", 18)} ${t("UrgentDonate")}</div> <div id="popup-backdrop" onclick="hideAllPopups()"></div>
<div id="home">
<div id="urgent-notice" class="urgent-notice explanation" onclick="popup('about', 1, 'donate')" style="visibility: hidden;">${emoji("💖", 18)} ${t("UrgentDonate")}</div>
<div id="cobalt-main-box" class="center" style="visibility: hidden;"> <div id="cobalt-main-box" class="center" style="visibility: hidden;">
<div id="logo">${appName}</div> <div id="logo">${t("AppTitleCobalt")}</div>
<div id="download-area"> <div id="download-area">
<div id="top"> <div id="top">
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input> <input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input>
@ -382,19 +463,18 @@ export default function(obj) {
name: "audioMode", name: "audioMode",
noParent: true, noParent: true,
items: [{ items: [{
"action": "false", action: "false",
"text": `${emoji("✨")} ${t("ModeToggleAuto")}` text: `${emoji("✨")} ${t("ModeToggleAuto")}`
}, { }, {
"action": "true", action: "true",
"text": `${emoji("🎶")} ${t("ModeToggleAudio")}` text: `${emoji("🎶")} ${t("ModeToggleAudio")}`
}] }]
})} })}
</div> </div>
</div> </div>
</div> </div>
<footer id="footer" style="visibility: hidden;"> <footer id="footer" style="visibility: hidden;">
${/* big action buttons are ALWAYS either first or last, because usual buttons are bundled in pairs and are sandwiched between bigger buttons for mobile view */ ${footerButtons([{
footerButtons([{
name: "about", name: "about",
type: "popup", type: "popup",
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`, text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
@ -412,6 +492,7 @@ export default function(obj) {
aria: t('AccessibilityOpenSettings') aria: t('AccessibilityOpenSettings')
}])} }])}
</footer> </footer>
</div>
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
const loc = { const loc = {
@ -422,12 +503,16 @@ export default function(obj) {
pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `, pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `,
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `, pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `, pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
pickerDefaultExpl: ` + "`" + t(`MediaPickerExplanation${isMobile ? `Phone${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `, pickerDefaultExpl: ` + "`" + t(`MediaPickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
featureErrorGeneric: ` + "`" + t('FeatureErrorGeneric') + "`" + `,
clipboardErrorNoPermission: ` + "`" + t('ClipboardErrorNoPermission') + "`" + `,
clipboardErrorFirefox: ` + "`" + t('ClipboardErrorFirefox') + "`" + `,
}; };
let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}'; let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}';
</script> </script>
<script type="text/javascript" src="cobalt.js"></script> <script type="text/javascript" src="cobalt.js"></script>
</html>`; </html>
`
} catch (err) { } catch (err) {
return `${t('ErrorPageRenderFail', obj.hash)}`; return `${t('ErrorPageRenderFail', obj.hash)}`;
} }

View file

@ -69,6 +69,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted) {
u: Array.isArray(r.urls) ? r.urls[0] : r.urls, u: Array.isArray(r.urls) ? r.urls[0] : r.urls,
mute: true mute: true
} }
if (host === "reddit" && r.typeId === 1) responseType = 1;
break; break;
case "picker": case "picker":

View file

@ -11,17 +11,25 @@ export default async function(obj) {
if (!("reddit_video" in data["secure_media"])) return { error: 'ErrorEmptyDownload' }; if (!("reddit_video" in data["secure_media"])) return { error: 'ErrorEmptyDownload' };
if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] }; if (data["secure_media"]["reddit_video"]["duration"] * 1000 > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0], let audio = false,
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`; video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
await fetch(audio, { method: "HEAD" }).then((r) => {if (Number(r.status) !== 200) audio = ''}).catch(() => {audio = ''}); audioFileLink = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
let id = data["secure_media"]["reddit_video"]["fallback_url"].split('/')[3]; await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
if (!audio.length > 0) return { typeId: 1, urls: video };
// fallback for videos with differentiating audio quality
if (!audio) {
audioFileLink = `${video.split('_')[0]}_AUDIO_128.mp4`
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
}
let id = video.split('/')[3];
if (!audio) return { typeId: 1, urls: video };
return { return {
typeId: 2, typeId: 2,
type: "render", type: "render",
urls: [video, audio], urls: [video, audioFileLink],
audioFilename: `reddit_${id}_audio`, audioFilename: `reddit_${id}_audio`,
filename: `reddit_${id}.mp4` filename: `reddit_${id}.mp4`
}; };

View file

@ -12,7 +12,7 @@
"enabled": true "enabled": true
}, },
"twitter": { "twitter": {
"alias": "twitter posts & voice", "alias": "twitter videos & voice",
"patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"], "patterns": [":user/status/:id", ":user/status/:id/video/:v", "i/spaces/:spaceId"],
"enabled": true "enabled": true
}, },

View file

@ -3,6 +3,8 @@ import { createInterface } from "readline";
import { Cyan, Bright } from "./sub/consoleText.js"; import { Cyan, Bright } from "./sub/consoleText.js";
import { execSync } from "child_process"; import { execSync } from "child_process";
import { version } from "../modules/config.js";
let envPath = './.env'; let envPath = './.env';
let q = `${Cyan('?')} \x1b[1m`; let q = `${Cyan('?')} \x1b[1m`;
let ob = {}; let ob = {};
@ -24,7 +26,7 @@ let final = () => {
} }
console.log( console.log(
`${Cyan("Hey, this is cobalt.")}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}` `${Cyan(`Hey, this is cobalt v.${version}!`)}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
) )
console.log( console.log(

View file

@ -153,3 +153,8 @@ export function getThreads() {
return '0' return '0'
} }
} }
export function cleanHTML(html) {
let clean = html.replace(/ {4}/g, '');
clean = clean.replace(/\n/g, '');
return clean
}

View file

@ -790,7 +790,7 @@
}], }],
"reddit": [{ "reddit": [{
"name": "video with audio", "name": "video with audio",
"url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3", "url": "https://www.reddit.com/r/TikTokCringe/comments/wup1fg/id_be_escaping_at_the_first_chance_i_got/?utm_source=share&utm_medium=web2x&context=3",
"params": {}, "params": {},
"expected": { "expected": {
"code": 200, "code": 200,
@ -798,7 +798,7 @@
} }
}, { }, {
"name": "video with audio (isAudioOnly)", "name": "video with audio (isAudioOnly)",
"url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3", "url": "https://www.reddit.com/r/TikTokCringe/comments/wup1fg/id_be_escaping_at_the_first_chance_i_got/?utm_source=share&utm_medium=web2x&context=3",
"params": { "params": {
"isAudioOnly": true "isAudioOnly": true
}, },
@ -808,7 +808,7 @@
} }
}, { }, {
"name": "video with audio (isAudioMuted)", "name": "video with audio (isAudioMuted)",
"url": "https://www.reddit.com/r/catvideos/comments/b2rygq/my_new_kittens_1st_day_checking_out_his_new_home/?utm_source=share&utm_medium=web2x&context=3", "url": "https://www.reddit.com/r/TikTokCringe/comments/wup1fg/id_be_escaping_at_the_first_chance_i_got/?utm_source=share&utm_medium=web2x&context=3",
"params": { "params": {
"isAudioMuted": true "isAudioMuted": true
}, },
@ -832,6 +832,14 @@
"code": 200, "code": 200,
"status": "redirect" "status": "redirect"
} }
}, {
"name": "different audio link, live render",
"url": "https://www.reddit.com/r/TikTokCringe/comments/15hce91/asian_daddy_kink/",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}], }],
"instagram": [{ "instagram": [{
"name": "several videos in a post (picker)", "name": "several videos in a post (picker)",