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",
"description": "save what you love",
"version": "6.3.1",
"version": "7.0-dev",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",

View file

@ -24,13 +24,13 @@ app.disable('x-powered-by');
await loadLoc();
const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port))
const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port))
const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port));
const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port));
if (apiMode) {
runAPI(express, app, gitCommit, gitBranch, __dirname);
runAPI(express, app, gitCommit, gitBranch, __dirname)
} else if (webMode) {
await runWeb(express, app, gitCommit, gitBranch, __dirname);
await runWeb(express, app, gitCommit, gitBranch, __dirname)
} else {
console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`));
console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`))
}

View file

@ -4,40 +4,45 @@ import { randomBytes } from "crypto";
const ipSalt = randomBytes(64).toString('hex');
import { appName, version } from "../modules/config.js";
import { version } from "../modules/config.js";
import { getJSON } from "../modules/api.js";
import { apiJSON, checkJSONPost, getIP, languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js";
import stream from "../modules/stream/stream.js";
import loc from "../localization/manager.js";
import { changelogHistory } from "../modules/pageRender/onDemand.js";
import { sha256 } from "../modules/sub/crypto.js";
import { celebrationsEmoji } from "../modules/pageRender/elements.js";
import { verifyStream } from "../modules/stream/manage.js";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {};
const corsConfig = process.env.cors === '0' ? {
origin: process.env.webURL,
optionsSuccessStatus: 200
} : {};
const apiLimiter = rateLimit({
windowMs: 60000,
max: 20,
standardHeaders: false,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
return res.status(429).json({
"status": "error",
"text": loc(languageCode(req), 'ErrorRateLimit')
});
}
});
const apiLimiterStream = rateLimit({
windowMs: 60000,
max: 25,
standardHeaders: false,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req, res) => sha256(getIP(req), ipSalt),
handler: (req, res, next, opt) => {
res.status(429).json({ "status": "error", "text": loc(languageCode(req), 'ErrorRateLimit') });
return;
return res.status(429).json({
"status": "error",
"text": loc(languageCode(req), 'ErrorRateLimit')
});
}
});
@ -55,45 +60,55 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
});
app.use('/api/json', express.json({
verify: (req, res, buf) => {
try {
JSON.parse(buf);
let acceptCon = String(req.header('Accept')) === "application/json";
if (acceptCon) {
if (buf.length > 720) throw new Error();
if (String(req.header('Content-Type')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid content type header' });
return;
}
if (String(req.header('Accept')) !== "application/json") {
res.status(400).json({ 'status': 'error', 'text': 'invalid accept header' });
return;
}
} catch(e) {
res.status(400).json({ 'status': 'error', 'text': 'invalid json body.' });
return;
JSON.parse(buf);
} else {
throw new Error();
}
}
}));
// handle express.json errors properly (https://github.com/expressjs/express/issues/4065)
app.use('/api/json', (err, req, res, next) => {
let errorText = "invalid json body";
let acceptCon = String(req.header('Accept')) !== "application/json";
if (err || acceptCon) {
if (acceptCon) errorText = "invalid accept header";
return res.status(400).json({
status: "error",
text: errorText
});
} else {
next();
}
});
app.post('/api/json', async (req, res) => {
try {
let lang = languageCode(req);
let j = apiJSON(0, { t: "Bad request" });
let j = apiJSON(0, { t: "bad request" });
try {
let contentCon = String(req.header('Content-Type')) === "application/json";
let request = req.body;
if (request.url) {
if (contentCon && request.url) {
request.dubLang = request.dubLang ? lang : false;
let chck = checkJSONPost(request);
j = chck ? await getJSON(chck["url"], lang, chck) : apiJSON(0, { t: loc(lang, 'ErrorCouldntFetch') });
if (!chck) throw new Error();
j = await getJSON(chck["url"], lang, chck);
} else {
j = apiJSON(0, { t: loc(lang, 'ErrorNoLink') });
j = apiJSON(0, {
t: !contentCon ? "invalid content type header" : loc(lang, 'ErrorNoLink')
});
}
} catch (e) {
j = apiJSON(0, { t: loc(lang, 'ErrorCantProcess') });
}
res.status(j.status).json(j.body);
return;
return res.status(j.status).json(j.body);
} catch (e) {
res.destroy();
return
return res.destroy();
}
});
@ -105,49 +120,23 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
&& req.query.h.toString().length === 64 && req.query.e.toString().length === 13) {
let streamInfo = verifyStream(req.query.t, req.query.h, req.query.e);
if (streamInfo.error) {
res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
return;
return res.status(streamInfo.status).json(apiJSON(0, { t: streamInfo.error }).body);
}
if (req.query.p) {
res.status(200).json({ "status": "continue" });
return;
return res.status(200).json({
status: "continue"
});
}
stream(res, streamInfo);
return stream(res, streamInfo);
} else {
let j = apiJSON(0, { t: "stream token, hmac, or expiry timestamp is missing." })
res.status(j.status).json(j.body);
return;
}
break;
case 'onDemand':
if (req.query.blockId) {
let blockId = req.query.blockId.slice(0, 3);
let r, j;
switch(blockId) {
case "0": // changelog history
r = changelogHistory();
j = r ? apiJSON(3, { t: r }) : apiJSON(0, { t: "couldn't render this block" })
break;
case "1": // celebrations emoji
r = celebrationsEmoji();
j = r ? apiJSON(3, { t: r }) : false
break;
default:
j = apiJSON(0, { t: "couldn't find a block with this id" })
break;
}
if (j.body) {
res.status(j.status).json(j.body)
} else {
res.status(204).end()
}
} else {
let j = apiJSON(0, { t: "no block id" });
res.status(j.status).json(j.body)
let j = apiJSON(0, {
t: "stream token, hmac, or expiry timestamp is missing"
})
return res.status(j.status).json(j.body);
}
break;
case 'serverInfo':
res.status(200).json({
return res.status(200).json({
version: version,
commit: gitCommit,
branch: gitBranch,
@ -158,13 +147,17 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
});
break;
default:
let j = apiJSON(0, { t: "unknown response type" })
res.status(j.status).json(j.body);
let j = apiJSON(0, {
t: "unknown response type"
})
return res.status(j.status).json(j.body);
break;
}
} catch (e) {
res.status(500).json({ 'status': 'error', 'text': loc(languageCode(req), 'ErrorCantProcess') });
return;
return res.status(500).json({
status: "error",
text: loc(languageCode(req), 'ErrorCantProcess')
});
}
});
app.get('/api/status', (req, res) => {
@ -178,6 +171,11 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
});
app.listen(process.env.apiPort, () => {
console.log(`\n${Cyan(appName)} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\nURL: ${Cyan(`${process.env.apiURL}`)}\nPort: ${process.env.apiPort}\n`)
console.log(`\n` +
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.apiURL}`)}\n` +
`Port: ${process.env.apiPort}\n`
)
});
}

View file

@ -1,20 +1,18 @@
import { appName, genericUserAgent, version } from "../modules/config.js";
import { languageCode } from "../modules/sub/utils.js";
import { genericUserAgent, version } from "../modules/config.js";
import { apiJSON, languageCode } from "../modules/sub/utils.js";
import { Bright, Cyan } from "../modules/sub/consoleText.js";
import { buildFront } from "../modules/build.js";
import findRendered from "../modules/pageRender/findRendered.js";
// * will be removed in the future
import cors from "cors";
// *
import { celebrationsEmoji } from "../modules/pageRender/elements.js";
import { changelogHistory } from "../modules/pageRender/onDemand.js";
export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
await buildFront(gitCommit, gitBranch);
const startTime = new Date();
const startTimestamp = Math.floor(startTime.getTime());
// * will be removed in the future
const corsConfig = process.env.cors === '0' ? { origin: process.env.webURL, optionsSuccessStatus: 200 } : {};
app.use('/api/:type', cors(corsConfig));
// *
await buildFront(gitCommit, gitBranch);
app.use('/', express.static('./build/min'));
app.use('/', express.static('./src/front'));
@ -23,29 +21,67 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
try { decodeURIComponent(req.path) } catch (e) { return res.redirect('/') }
next();
});
app.get('/onDemand', (req, res) => {
try {
if (req.query.blockId) {
let blockId = req.query.blockId.slice(0, 3);
let r, j;
switch(blockId) {
// changelog history
case "0":
r = changelogHistory();
j = r ? apiJSON(3, { t: r }) : apiJSON(0, {
t: "couldn't render this block, please try again!"
})
break;
// celebrations emoji
case "1":
r = celebrationsEmoji();
j = r ? apiJSON(3, { t: r }) : false
break;
default:
j = apiJSON(0, {
t: "couldn't find a block with this id"
})
break;
}
if (j.body) {
return res.status(j.status).json(j.body);
} else {
return res.status(204).end();
}
} else {
return res.status(400).json({
status: "error",
text: "couldn't render this block, please try again!"
});
}
} catch (e) {
return res.status(400).json({
status: "error",
text: "couldn't render this block, please try again!"
})
}
});
app.get("/status", (req, res) => {
res.status(200).end()
return res.status(200).end()
});
app.get("/", (req, res) => {
res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`)
return res.sendFile(`${__dirname}/${findRendered(languageCode(req), req.header('user-agent') ? req.header('user-agent') : genericUserAgent)}`)
});
app.get("/favicon.ico", (req, res) => {
res.sendFile(`${__dirname}/src/front/icons/favicon.ico`)
return res.sendFile(`${__dirname}/src/front/icons/favicon.ico`)
});
// * will be removed in the future
app.get("/api/*", (req, res) => {
res.redirect(308, process.env.apiURL.slice(0, -1) + req.url)
});
app.post("/api/*", (req, res) => {
res.redirect(308, process.env.apiURL.slice(0, -1) + req.url)
});
// *
app.get("/*", (req, res) => {
res.redirect('/')
return res.redirect('/')
});
app.listen(process.env.webPort, () => {
let startTime = new Date();
console.log(`\n${Cyan(appName)} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\nStart time: ${Bright(`${startTime.toUTCString()} (${Math.floor(new Date().getTime())})`)}\n\nURL: ${Cyan(`${process.env.webURL}`)}\nPort: ${process.env.webPort}\n`)
console.log(`\n` +
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.webURL}`)}\n` +
`Port: ${process.env.webPort}\n`
)
})
}

View file

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

View file

@ -1,6 +1,8 @@
const ua = navigator.userAgent.toLowerCase();
const isIOS = ua.match("iphone os");
const isMobile = ua.match("android") || ua.match("iphone os");
const isFirefox = ua.match("firefox/");
const isOldFirefox = ua.match("firefox/") && ua.split("firefox/")[1].split('.')[0] < 103;
const version = 31;
const regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
const notification = `<div class="notification-dot"></div>`;
@ -14,10 +16,11 @@ const switchers = {
"vimeoDash": ["false", "true"],
"audioMode": ["false", "true"]
};
const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio"];
const checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "muteAudio", "reduceTransparency", "disableAnimations"];
const exceptions = { // used for mobile devices
"vQuality": "720"
};
const bottomPopups = ["error", "download"]
let store = {};
@ -156,16 +159,18 @@ function notificationCheck(type) {
function hideAllPopups() {
let filter = document.getElementsByClassName('popup');
for (let i = 0; i < filter.length; i++) {
filter[i].style.visibility = "hidden";
filter[i].classList.remove("visible");
}
eid("picker-holder").innerHTML = '';
eid("picker-download").href = '/';
eid("picker-download").style.visibility = "hidden";
eid("popup-backdrop").style.visibility = "hidden";
eid("picker-download").classList.remove("visible");
eid("popup-backdrop").classList.remove("visible");
store.isPopupOpen = false;
}
function popup(type, action, text) {
if (action === 1) {
hideAllPopups(); // hide the previous popup before showing a new one
store.isPopupOpen = true;
switch (type) {
case "about":
let tabId = sGet("seenAbout") ? "changelog" : "about";
@ -192,7 +197,7 @@ function popup(type, action, text) {
if (!eid("popup-picker").classList.contains("scrollable")) eid("popup-picker").classList.add("scrollable");
if (eid("picker-holder").classList.contains("various")) eid("picker-holder").classList.remove("various");
eid("picker-download").href = text.audio;
eid("picker-download").style.visibility = "visible"
eid("picker-download").classList.add("visible");
for (let i in text.arr) {
eid("picker-holder").innerHTML += `<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;
switch (s.type) {
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;
}
eid("picker-holder").innerHTML += item
}
eid("picker-download").style.visibility = "hidden";
eid("picker-download").classList.remove("visible");
break;
}
break;
@ -219,14 +224,17 @@ function popup(type, action, text) {
break;
}
} else {
store.isPopupOpen = false;
if (type === "picker") {
eid("picker-download").href = '/';
eid("picker-download").style.visibility = "hidden"
eid("picker-download").classList.remove("visible");
eid("picker-holder").innerHTML = ''
}
}
eid("popup-backdrop").style.visibility = vis(action);
eid(`popup-${type}`).style.visibility = vis(action);
if (bottomPopups.includes(type)) eid(`popup-${type}-container`).classList.toggle("visible");
eid("popup-backdrop").classList.toggle("visible");
eid(`popup-${type}`).classList.toggle("visible");
eid(`popup-${type}`).focus();
}
function changeSwitcher(li, b) {
if (b) {
@ -249,15 +257,12 @@ function checkbox(action) {
sSet(action, !!eid(action).checked);
switch(action) {
case "alwaysVisibleButton": button(); break;
case "reduceTransparency": eid("cobalt-body").classList.toggle('no-transparency'); break;
case "disableAnimations": eid("cobalt-body").classList.toggle('no-animation'); break;
}
action === "disableChangelog" && sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
}
function loadSettings() {
try {
if (typeof(navigator.clipboard.readText) == "undefined") throw new Error();
} catch (err) {
eid("paste").style.display = "none";
}
if (sGet("alwaysVisibleButton") === "true") {
eid("alwaysVisibleButton").checked = true;
eid("download-button").value = '>>'
@ -266,6 +271,12 @@ function loadSettings() {
if (sGet("downloadPopup") === "true" && !isIOS) {
eid("downloadPopup").checked = true;
}
if (sGet("reduceTransparency") === "true" || isOldFirefox) {
eid("cobalt-body").classList.toggle('no-transparency');
}
if (sGet("disableAnimations") === "true") {
eid("cobalt-body").classList.toggle('no-animation');
}
for (let i = 0; i < checkboxes.length; i++) {
if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true;
}
@ -312,7 +323,17 @@ async function pasteClipboard() {
eid("url-input-area").value = t;
download(eid("url-input-area").value);
}
} catch (e) {}
} catch (e) {
let errorMessage = loc.featureErrorGeneric;
let doError = true;
e = String(e).toLowerCase();
if (e.includes("denied")) errorMessage = loc.clipboardErrorNoPermission;
if (e.includes("dismissed")) doError = false;
if (e.includes("function") && isFirefox) errorMessage = loc.clipboardErrorFirefox;
if (doError) popup("error", 1, errorMessage);
}
}
async function download(url) {
changeDownloadButton(2, '...');
@ -409,7 +430,7 @@ async function download(url) {
async function loadCelebrationsEmoji() {
let bac = eid("about-footer").innerHTML;
try {
let j = await fetch(`${apiURL}/api/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false });
let j = await fetch(`/onDemand?blockId=1`).then((r) => { if (r.status === 200) { return r.json() } else { return false } }).catch(() => { return false });
if (j && j.status === "success" && j.text) {
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace('<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) {
j = store.historyContent;
} else {
await fetch(`${apiURL}/api/onDemand?blockId=${blockId}`).then(async(r) => {
await fetch(`/onDemand?blockId=${blockId}`).then(async(r) => {
j = await r.json();
if (j && j.status === "success") {
store.historyContent = j;
@ -461,14 +482,28 @@ window.onload = () => {
button();
}
}
eid("url-input-area").addEventListener("keydown", (event) => {
if (event.key === 'Escape') eid("url-input-area").value = '';
eid("url-input-area").addEventListener("keydown", (e) => {
button();
})
eid("url-input-area").addEventListener("keyup", (event) => {
if (event.key === 'Enter') eid("download-button").click();
eid("url-input-area").addEventListener("keyup", (e) => {
if (e.key === 'Enter') eid("download-button").click();
})
document.onkeydown = (event) => {
if (event.key === "Tab" || event.ctrlKey) eid("url-input-area").focus();
if (event.key === 'Escape') hideAllPopups();
document.onkeydown = (e) => {
if (!store.isPopupOpen) {
if (e.ctrlKey || e.key === "/") eid("url-input-area").focus();
if (e.key === "Escape" || e.key === "Clear" || e.key === "Delete") clearInput();
// top buttons
if (e.key === "D") pasteClipboard();
if (e.key === "K") changeSwitcher('audioMode', 'false');
if (e.key === "L") changeSwitcher('audioMode', 'true');
// popups
if (e.key === "B") popup('about', 1);
if (e.key === "N") popup('about', 1, 'donate');
if (e.key === "M") popup('settings', 1);
} else {
if (e.key === "Escape") hideAllPopups();
}
}

View file

@ -4,21 +4,21 @@
"ContactLink": "<a class=\"text-backdrop italic\" href=\"{repo}\" target=\"_blank\">create an issue on github</a>"
},
"strings": {
"AppTitleCobalt": "cobalt",
"LinkInput": "paste the link here",
"AboutSummary": "{appName} is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
"AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
"EmbedBriefDescription": "save what you love without ads, trackers, or other creepy bullshit.",
"MadeWithLove": "made with <3 by wukko",
"AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup",
"AccessibilityDownloadButton": "download button",
"AccessibilityOpenSettings": "open settings popup",
"AccessibilityClosePopup": "close the popup",
"AccessibilityOpenDonate": "open donation popup",
"TitlePopupAbout": "what's {appName}?",
"TitlePopupAbout": "what's cobalt?",
"TitlePopupSettings": "settings",
"TitlePopupError": "uh-oh...",
"TitlePopupChangelog": "what's new?",
"TitlePopupDonate": "support {appName}",
"TitlePopupDonate": "support cobalt",
"TitlePopupDownload": "how to save?",
"ErrorSomethingWentWrong": "something went wrong and i couldn't get anything for you. try again, but if issue persists, {ContactLink}.",
"ErrorUnsupported": "it seems like this service is not supported yet or your link is invalid. have you pasted the right link?",
@ -29,8 +29,8 @@
"ErrorCouldntFetch": "i couldn't find anything about this link. check if it works and try again! some content may be region restricted, so keep that in mind.",
"ErrorLengthLimit": "i can't process videos longer than {s} minutes, so pick something shorter instead!",
"ErrorBadFetch": "something went wrong when i tried getting info about your link. are you sure it works? check if it does, and try again.",
"ErrorNoInternet": "there's no internet or {appName} api is temporarily unavailable. check your connection and try again.",
"ErrorCantConnectToServiceAPI": "i couldn't connect to the service api. maybe it's down, or {appName} got blocked. try again, but if error persists, {ContactLink}.",
"ErrorNoInternet": "there's no internet or cobalt api is temporarily unavailable. check your connection and try again.",
"ErrorCantConnectToServiceAPI": "i couldn't connect to the service api. maybe it's down, or cobalt got blocked. try again, but if error persists, {ContactLink}.",
"ErrorEmptyDownload": "i don't see anything i could download by your link. try a different one!",
"ErrorLiveVideo": "this is a live video, i am yet to learn how to look into future. wait for the stream to finish and try again!",
"SettingsAppearanceSubtitle": "appearance",
@ -46,7 +46,7 @@
"AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsQualityDescription": "if selected quality isn't available, closest one is used instead.",
"LinkGitHubChanges": "&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\".",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"ClickToCopy": "press to copy",
@ -87,13 +87,12 @@
"MediaPickerTitle": "pick what to save",
"MediaPickerExplanationPC": "click or right click to download what you want.",
"MediaPickerExplanationPhone": "press or press and hold to download what you want.",
"MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save.",
"TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!",
"ErrorCantProcess": "i couldn't process your request :(\nyou can try again, but if issue persists, please {ContactLink}.",
"ChangelogPressToHide": "collapse",
"Donate": "donate",
"DonateSub": "help it stay online",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, meaning that <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",
"DonateHireMe": "...or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a> :)",
"SettingsVideoMute": "mute audio",
@ -103,24 +102,32 @@
"CollapseSupport": "support & source code",
"CollapsePrivacy": "privacy policy",
"ServicesNote": "this list is not final and keeps expanding over time, make sure to check it once in a while!",
"FollowSupport": "keep in touch with {appName} for support, polls, news, and more:",
"SupportNote": "please note that questions and issues may take a while to respond to, there's only one person managing everything.",
"FollowSupport": "keep in touch with cobalt for support, polls, news, and more:",
"SupportNote": "please note that response may take a while, there's only one person managing everything.",
"SourceCode": "report issues, explore source code, star or fork the repo:",
"PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is your business, not mine.\n\nsome non-backtraceable data does get temporarily stored when requested download requires live render. it's necessary for that feature to function.\n\nin that case, information about requested stream is temporarily stored in server's RAM for <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!",
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nnote: youtube api sometimes acts unexpectedly. blame google for this, not me.",
"SettingsCodecSubtitle": "youtube codec",
"SettingsCodecDescription": "h264: generally better player support, but quality tops out at 1080p.\nav1: low player support, but supports 8k & HDR.\nvp9: usually highest bitrate, preserves most detail. supports 4k & HDR.\n\npick h264 if you want best editor/player/social media compatibility.",
"SettingsAudioDub": "youtube audio track",
"SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and {appName}) language is used.",
"SettingsAudioDubDescription": "defines which audio track will be used. if dubbed track isn't available, original video language is used instead.\n\noriginal: original video language is used.\nauto: default browser (and cobalt) language is used.",
"SettingsDubDefault": "original",
"SettingsDubAuto": "auto",
"SettingsVimeoPrefer": "vimeo downloads type",
"SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by {appName} into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.",
"SettingsVimeoPreferDescription": "progressive: direct file link to vimeo's cdn. max quality is 1080p.\ndash: video and audio are merged by cobalt into one file. max quality is 4k.\n\npick \"progressive\" if you want best editor/player/social media compatibility. if progressive download isn't available, dash is used instead.",
"ShareURL": "share",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
"ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.",
"UrgentDonate": "{appName} needs your help!",
"PopupCloseDone": "done"
"UrgentDonate": "cobalt needs your help!",
"PopupCloseDone": "done",
"Accessibility": "accessibility",
"SettingsReduceTransparency": "reduce transparency",
"SettingsDisableAnimations": "disable animations",
"FeatureErrorGeneric": "your browser doesn't allow or support this feature. check if there are any updates available and try again!",
"ClipboardErrorFirefox": "you're using firefox where all clipboard reading functionality is disabled.\n\nyou can fix this by following steps listed <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>"
},
"strings": {
"AppTitleCobalt": "кобальт",
"LinkInput": "вставь ссылку сюда",
"AboutSummary": "{appName} - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.",
"MadeWithLove": "сделано wukko, с <3",
"AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания",
"AccessibilityOpenSettings": "открыть настройки",
"AccessibilityClosePopup": "закрыть окно",
"AccessibilityOpenDonate": "сделать пожертвование",
"TitlePopupAbout": "что за {appName}?",
"TitlePopupAbout": "что за кобальт?",
"TitlePopupSettings": "настройки",
"TitlePopupError": "опаньки...",
"TitlePopupChangelog": "что нового?",
"TitlePopupDonate": "поддержи {appName}",
"TitlePopupDonate": "поддержи кобальт",
"TitlePopupDownload": "как сохранить?",
"ErrorSomethingWentWrong": "что-то пошло совсем не так и у меня не получилось ничего для тебя достать. попробуй ещё раз, но если так и не получится, {ContactLink}.",
"ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?",
@ -30,7 +30,7 @@
"ErrorLengthLimit": "я не могу обрабатывать видео длиннее чем {s} минут(ы), так что скачай что-нибудь покороче!",
"ErrorBadFetch": "произошла какая-то ошибка при получении данных по твоей ссылке. убедись, что она работает, и попробуй ещё раз.",
"ErrorNoInternet": "не получилось подключиться к серверу. проверь подключение к интернету и попробуй ещё раз!",
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же {appName} заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.",
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу этого сервиса. возможно он лежит, или же кобальт заблокировали. попробуй ещё раз, но если так и не получится, {ContactLink}.",
"ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!",
"ErrorLiveVideo": "я пока что не умею заглядывать в будущее, поэтому дождись окончания прямого эфира, и потом уже скачивай видео!",
"SettingsAppearanceSubtitle": "внешний вид",
@ -46,13 +46,13 @@
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.",
"LinkGitHubChanges": "&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, выбери скачанный файл, нажми иконку \"поделиться\", и, наконец, нажми \"сохранить видео\".",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода скачивания файла в настройках.",
"ClickToCopy": "нажми, чтобы скопировать",
"Download": "скачать",
"CopyURL": "скопировать",
"AboutTab": "о {appName}",
"AboutTab": "о кобальте",
"ChangelogTab": "изменения",
"DonationsTab": "донаты",
"SettingsVideoTab": "видео",
@ -93,7 +93,7 @@
"ChangelogPressToHide": "скрыть",
"Donate": "задонатить",
"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": "открыть",
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
"SettingsVideoMute": "убрать аудио",
@ -103,24 +103,32 @@
"CollapseSupport": "поддержка и исходный код",
"CollapsePrivacy": "политика конфиденциальности",
"ServicesNote": "этот список далеко не финальный и постоянно пополняется. заглядывай сюда почаще, тогда точно будешь знать, что поддерживается!",
"FollowSupport": "оставайтесь на связи с {appName} для новостей, поддержки, участия в опросах, и многого другого:",
"FollowSupport": "оставайтесь на связи с кобальтом для новостей, поддержки, участия в опросах, и многого другого:",
"SupportNote": "так как я один занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.",
"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": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!",
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
"SettingsCodecSubtitle": "кодек для видео с youtube",
"SettingsCodecDescription": "h264: обширная поддержка плеерами, но макс. качество всего лишь 1080p.\nav1: слабая поддержка плеерами, но поддерживает 8k и HDR.\nvp9: обычно наиболее высокий битрейт, лучше сохраняется качество видео. поддерживает 4k и HDR.\n\nвыбирай h264, если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями.",
"SettingsAudioDub": "звуковая дорожка для видео с youtube",
"SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера (и {appName}).",
"SettingsAudioDubDescription": "определяет, какая звуковая дорожка используется при скачивании видео. если дублированная дорожка недоступна, то вместо неё используется оригинальная.\n\nоригинал: используется оригинальная дорожка.\nавто: используется язык браузера и интерфейса кобальта.",
"SettingsDubDefault": "оригинал",
"SettingsDubAuto": "авто",
"SettingsVimeoPrefer": "тип загрузок с vimeo",
"SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: {appName} совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, {appName} скачает \"dash\".",
"SettingsVimeoPreferDescription": "progressive: прямая ссылка на файл с сервера vimeo. максимальное качество: 1080p.\ndash: кобальт совмещает видео и аудио в один файл. максимальное качество: 4k.\n\nвыбирай \"progressive\", если тебе нужна наилучшая совместимость с плеерами/редакторами/соцсетями. если \"progressive\" файл недоступен, кобальт скачает \"dash\".",
"ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
"UrgentDonate": "нужна твоя помощь!",
"PopupCloseDone": "готово"
"PopupCloseDone": "готово",
"Accessibility": "общедоступность",
"SettingsReduceTransparency": "уменьшить прозрачность",
"SettingsDisableAnimations": "выключить анимации",
"FeatureErrorGeneric": "твой браузер не разрешает или не поддерживает эту функцию. проверь наличие обновлений и попробуй ещё раз!",
"ClipboardErrorFirefox": "ты используешь firefox в котором все функции чтения из буфера обмена отключены по умолчанию.\n\nно это можно исправить следуя шагам, описанным <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 { appName, links, repo } from "../modules/config.js";
import { links, repo } from "../modules/config.js";
import loadJson from "../modules/sub/loadJSON.js";
const locPath = './src/localization/languages';
@ -16,7 +16,7 @@ export async function loadLoc() {
}
export function replaceBase(s) {
return s.replace(/\n/g, '<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) {
let s = replaceBase(str[string])

View file

@ -1,14 +1,10 @@
import * as esbuild from "esbuild";
import * as fs from "fs";
import { loadLoc, languageList } from "../localization/manager.js";
import { cleanHTML } from "./sub/utils.js";
import page from "./pageRender/page.js";
function cleanHTML(html) {
let clean = html.replace(/ {4}/g, '');
clean = clean.replace(/\n/g, '');
return clean
}
export async function buildFront(commitHash, branch) {
try {
// preload localization files

View file

@ -1,11 +1,18 @@
{
"current": {
"version": "7.0",
"date": "August 4, 2023",
"title": "wip: ui refresh and more!",
"banner": "cattired.webp",
"content": "hey beta testers, this changelog isn't final but i do want to highlight some changes here just to keep track of them. make sure to report all issues in the testing discord channel!\n\n(this changelog is not sorted as it usually is)\n\nservice improvements:\n*; fixed unexpected stream drop when downloading a silent reddit video with mute mode on.\n*; added support for new reddit audio link type.\n\nweb improvements:\n*; removed 6.0 api fallback.\n*; moved on demand blocks to web server, now changelog can be updated independently from preferred api server.\n*; all-new matte glass aesthetic, applied to revamped popup headers, tab selectors, and also small popups.\n*; optimized installed web app to look and act like a native app, especially on ios. !!!!please try this!!!!\n*; added ability to attach a date to changelog.\n*; refreshed the look of entire changelog tab: separated title and version/commit, made title bigger, evened out all paddings.\n*; popups now work without any weird workarounds, especially on mobile. they're clean and nice.\n*; homescreen now also works without any weird workarounds. it is also clean and nice.\n*; replaced close button with back button, moved it to left. it makes more sense.\n*; (kinda old but not in older changelog) absolutely reimagined error and download popups, consistent with the rest of refreshed design.\n*; reduced spacing, optimized css of almost all ui elements. should be even more consistent across platforms now.\n*; added interaction animations.\n*; added more accessibility options, put them all into one category. you can disable animations and transparency if you want to.\n*; added a link to self-troubleshooting guide to support expand list in about popup.\n*; renamed 2160p and 4320p to 4k and 8k respectfully for better clarity.\n*; cobalt now lets you know if your browser doesn't support clipboard api and helps you fix it.\n*; added ability to translate \"cobalt\" for twitter-like localization. in russian cobalt is now кобальт, that's the style i will be going with from now on.\n*; updated some localization strings.\n*; removed ability to change the app name dynamically in all locations. cobalt is a sustained product name.\n*; \n*; added more keyboard shorcuts:\nshift+d: paste and download,\nshift+k: auto mode,\nshift+L: audio mode,\nshift+b: about popup,\nshift+n: donate popup,\nshift+m: settings popup.\n\non top of existing ones:\nctrl+v (without focusing anything): paste the link;\nescape/delete/clear: clear url input area\nescape: close current popup;\n\nyour keyboard slightly represents cobalt's ui. let me know if you like these.\n\ninternal web improvements:\n*; cleaned up all related frontend modules, especially page.js. will add more in final changelog, i'm very tired.\n\napi improvements:\n*; now catching all json api related errors.\n*; moved on demand blocks to web server.\n*; now sending standard rate limiting headers.\n*; better readability in source.\n\nother improvements:\n*; renamed docker-compose.yml.example to docker-compose.example.yml for linting in code editors.\n*; added a wiki with wip troubleshooting guide on github.\n\nwhat doesn't work or works poorly:\n*; tiktok/twitter media pickers look like shit, they haven't been worked on yet. they also might not work at all on ios.\n*; unknown if scrolling within popups works properly on ios 16 (when installed as web app).\n*; \"ask how to save\" toggle is pressable on ios devices even though it shouldn't be.\n\nwhat will surely be added in coming days:\n*; list of all keyboard shortcuts, probably a popup opened with a little button in left or right corner of the screen.\n*; proper dropdown arrow for about tab dropdowns.\n*; dates for all older changelogs.\n*; ...more?"
},
"history": [{
"version": "6.2",
"date": "June 27 2023",
"title": "all network issues have been fixed!",
"banner": "meowthhammer.webp",
"content": "hey! there have been some hiccups in cobalt's stability lately, i was going through finals while trying to scale up the infrastructure, and that didn't really work out, lol.\nBUT i'm happy to announce that i've optimized all nodes! <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",
"title": "better reliability, new infrastructure, pinterest support, and way more!",
"banner": "catswitchboxes.webp",

View file

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

View file

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

View file

@ -1,6 +1,10 @@
import { celebrations } from "../config.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) {
let items = ``;
if (obj.name === "download") {
@ -19,26 +23,18 @@ export function switcher(obj) {
${obj.explanation ? `<div class="explanation">${obj.explanation}</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) {
let paddingClass = ` `
switch (paddingType) {
case 1:
paddingClass += "bottom-margin"
break;
case 2:
paddingClass += "top-margin"
break;
case 3:
paddingClass += "no-margin"
break;
case 4:
paddingClass += "top-margin-only"
checkboxes += `<label id="${obj[i].action}-chkbx" class="checkbox${paddingClass}">
<input id="${obj[i].action}" type="checkbox" aria-label="${obj[i].aria ? obj[i].aria : obj[i].name}" onclick="checkbox('${obj[i].action}')">
<span>${obj[i].name}</span>
</label>`
}
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>`
return checkboxes
}
export function sep(paddingType) {
let paddingClass = ``
@ -50,7 +46,7 @@ export function sep(paddingType) {
return `<div class="separator${paddingClass}"></div>`
}
export function popup(obj) {
let classes = obj.classes ? obj.classes : []
let classes = obj.classes ? obj.classes : [];
let body = obj.body;
if (Array.isArray(obj.body)) {
body = ``
@ -65,37 +61,44 @@ export function popup(obj) {
}
}
return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center box${classes.length > 0 ? ' ' + classes.join(' ') : ''}" style="visibility: hidden;">` : ''}
<div id="popup-header" class="popup-header">
${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>` : ''}
${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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${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${!obj.buttonOnly ? " glass-bkg": ''}">
<div id="popup-header-contents">
${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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div>
</div>
<div id="popup-content"${obj.footer ? ' class="with-footer"' : ''}>
<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>` : ''}
</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>` : ''}`
}
export function multiPagePopup(obj) {
let tabs = ``
let tabContent = ``
let tabs = `
<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++) {
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>`
}
return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
${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.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}</div>` : ''}${tabContent}</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 id="popup-${obj.name}" class="popup center box scrollable">
<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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</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>`
}
export function collapsibleList(arr) {
@ -112,24 +115,28 @@ export function collapsibleList(arr) {
return items;
}
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++) {
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 `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
${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.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}</div>` : ''}${obj.content}</div>
<div id="popup-buttons" class="switches popup-tabs">${tabs}</div>
<div id="popup-${obj.name}" class="popup center box scrollable">
<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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</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>`
}
export function backdropLink(link, text) {
return `<a class="text-backdrop italic" href="${link}" target="_blank">${text}</a>`
}
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>`
}

View file

@ -1,4 +1,5 @@
import changelogManager from "../changelog/changelogManager.js"
import { cleanHTML } from "../sub/utils.js";
let cache = {}
@ -10,8 +11,16 @@ export function changelogHistory() { // blockId 0
let historyLen = history.length;
for (let i in history) {
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;
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 { services as s, appName, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
import { checkbox, collapsibleList, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher, socialLink } from "./elements.js";
import { services as s, authorInfo, version, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js";
import emoji from "../emoji.js";
@ -30,6 +30,7 @@ for (let i in donations["crypto"]) {
export default function(obj) {
const t = (str, replace) => { return loc(obj.lang, str, replace) };
let ua = obj.useragent.toLowerCase();
let isIOS = 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');
try {
return `<!DOCTYPE html>
return `
<!DOCTYPE html>
<html lang="${obj.lang}">
<head>
<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:title" content="${appName}" />
<meta property="og:title" content="${t("AppTitleCobalt")}" />
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
<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="theme-color" content="#000000" />
<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/png" sizes="32x32" href="icons/favicon-32x32.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="manifest" href="manifest.webmanifest" />
@ -69,9 +76,10 @@ export default function(obj) {
<noscript><div style="margin: 2rem;">${t('NoScriptMessage')}</div></noscript>
</head>
<body id="cobalt-body" ${platform === "p" ? 'class="desktop"' : ''} data-nosnippet ontouchstart>
<body id="notification-area"></div>
${multiPagePopup({
name: "about",
closeAria: t('AccessibilityClosePopup'),
closeAria: t('AccessibilityGoBack'),
tabs: [{
name: "about",
title: `${emoji("🐲")} ${t('AboutTab')}`,
@ -82,30 +90,40 @@ export default function(obj) {
text: t('MadeWithLove'),
url: authorInfo.link
},
closeAria: t('AccessibilityClosePopup'),
closeAria: t('AccessibilityGoBack'),
title: `${emoji("🔮", 30)} ${t('TitlePopupAbout')}`
},
body: [{
text: t('AboutSummary')
}, {
text: collapsibleList([{
"name": "services",
"title": t("CollapseServices"),
"body": `${enabledServices}<br/><br/>${t("ServicesNote")}`
name: "services",
title: t("CollapseServices"),
body: `${enabledServices}<br/><br/>${t("ServicesNote")}`
}, {
"name": "support",
"title": t("CollapseSupport"),
"body": `${t("FollowSupport")}<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/>
name: "support",
title: t("CollapseSupport"),
body: `
${t("SupportSelfTroubleshooting")}<br/>
${t("FollowSupport")}<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/>
${socialLink(emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo)}<br/>
${socialLink(
emoji("🐙"), "github", repo.replace("https://github.com/", ''), repo
)}<br/>
${t("SupportNote")}`
}, {
"name": "privacy",
"title": t("CollapsePrivacy"),
"body": t("PrivacyPolicy")
name: "privacy",
title: t("CollapsePrivacy"),
body: t("PrivacyPolicy")
}])
}]
})
@ -115,7 +133,7 @@ export default function(obj) {
content: popup({
name: "changelog",
header: {
closeAria: t('AccessibilityClosePopup'),
closeAria: t('AccessibilityGoBack'),
title: `${emoji("🪄", 30)} ${t('TitlePopupChangelog')}`
},
body: [{
@ -123,8 +141,14 @@ export default function(obj) {
raw: true
}, {
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
}, {
text: changelogManager("version"),
classes: ["changelog-tags"],
nopadding: true
}, {
text: changelogManager("title"),
classes: ["changelog-subtitle"],
@ -132,19 +156,26 @@ export default function(obj) {
}, {
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"],
nopadding: true
}, {
text: com[1]
}, {
text: backdropLink(`${repo}/commits`, t('LinkGitHubChanges')),
classes: ["bottom-link"]
}, {
text: `<div class="category-title">${t('ChangelogOlder')}</div>`,
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
}]
})
@ -154,14 +185,17 @@ export default function(obj) {
content: popup({
name: "donate",
header: {
closeAria: t('AccessibilityClosePopup'),
closeAria: t('AccessibilityGoBack'),
title: emoji("💸", 30) + t('TitlePopupDonate')
},
body: [{
text: `<div class="category-title">${t('DonateSub')}</div>`,
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
}, {
text: t('DonateExplanation')
@ -189,7 +223,7 @@ export default function(obj) {
})}
${multiPagePopup({
name: "settings",
closeAria: t('AccessibilityClosePopup'),
closeAria: t('AccessibilityGoBack'),
header: {
aboveTitle: {
text: `v.${version}-${obj.hash}${platform} (${obj.branch})`,
@ -207,33 +241,37 @@ export default function(obj) {
name: "vQuality",
explanation: t('SettingsQualityDescription'),
items: [{
"action": "max",
"text": "4320p+"
action: "max",
text: "8k+"
}, {
"action": "2160",
"text": "2160p"
action: "2160",
text: "4k"
}, {
"action": "1440",
"text": "1440p"
action: "1440",
text: "1440p"
}, {
"action": "1080",
"text": "1080p"
action: "1080",
text: "1080p"
}, {
"action": "720",
"text": "720p"
action: "720",
text: "720p"
}, {
"action": "480",
"text": "480p"
action: "480",
text: "480p"
}, {
"action": "360",
"text": "360p"
action: "360",
text: "360p"
}]
})
})
+ settingsCategory({
name: "tiktok",
title: "tiktok",
body: checkbox("disableTikTokWatermark", t('SettingsRemoveWatermark'), 3)
body: checkbox([{
action: "disableTikTokWatermark",
name: t("SettingsRemoveWatermark"),
padding: "no-margin"
}])
})
+ settingsCategory({
name: t('SettingsCodecSubtitle'),
@ -241,14 +279,14 @@ export default function(obj) {
name: "vCodec",
explanation: t('SettingsCodecDescription'),
items: [{
"action": "h264",
"text": "h264 (mp4)"
action: "h264",
text: "h264 (mp4)"
}, {
"action": "av1",
"text": "av1 (mp4)"
action: "av1",
text: "av1 (mp4)"
}, {
"action": "vp9",
"text": "vp9 (webm)"
action: "vp9",
text: "vp9 (webm)"
}]
})
})
@ -258,11 +296,11 @@ export default function(obj) {
name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'),
items: [{
"action": "false",
"text": "progressive"
action: "false",
text: "progressive"
}, {
"action": "true",
"text": "dash"
action: "true",
text: "dash"
}]
})
})
@ -272,31 +310,44 @@ export default function(obj) {
content: settingsCategory({
name: "general",
title: t('SettingsFormatSubtitle'),
body:
switcher({
name: "aFormat",
explanation: t('SettingsAudioFormatDescription'),
items: audioFormats
}) + sep(0) + checkbox("muteAudio", t('SettingsVideoMute'), 3) + explanation(t('SettingsVideoMuteExplanation'))
}) + settingsCategory({
name: "dub",
title: t("SettingsAudioDub"),
body: switcher({
name: "dubLang",
explanation: t('SettingsAudioDubDescription'),
items: [{
"action": "original",
"text": t('SettingsDubDefault')
}, {
"action": "auto",
"text": t('SettingsDubAuto')
}]
})
}) + settingsCategory({
name: "tiktok",
title: "tiktok",
body: checkbox("fullTikTokAudio", t('SettingsAudioFullTikTok'), 3) + explanation(t('SettingsAudioFullTikTokDescription'))
})
body: switcher({
name: "aFormat",
explanation: t('SettingsAudioFormatDescription'),
items: audioFormats
})
+ sep(0)
+ checkbox([{
action: "muteAudio",
name: t("SettingsVideoMute"),
padding: "no-margin"
}])
+ explanation(t('SettingsVideoMuteExplanation'))
})
+ settingsCategory({
name: "dub",
title: t("SettingsAudioDub"),
body: switcher({
name: "dubLang",
explanation: t('SettingsAudioDubDescription'),
items: [{
action: "original",
text: t('SettingsDubDefault')
}, {
action: "auto",
text: t('SettingsDubAuto')
}]
})
})
+ settingsCategory({
name: "tiktok",
title: "tiktok",
body: checkbox([{
action: "fullTikTokAudio",
name: t("SettingsAudioFullTikTok"),
padding: "no-margin"
}])
+ explanation(t('SettingsAudioFullTikTokDescription'))
})
}, {
name: "other",
title: `${emoji("🪅")} ${t('SettingsOtherTab')}`,
@ -307,26 +358,51 @@ export default function(obj) {
name: "theme",
subtitle: t('SettingsThemeSubtitle'),
items: [{
"action": "auto",
"text": t('SettingsThemeAuto')
action: "auto",
text: t('SettingsThemeAuto')
}, {
"action": "dark",
"text": t('SettingsThemeDark')
action: "dark",
text: t('SettingsThemeDark')
}, {
"action": "light",
"text": t('SettingsThemeLight')
action: "light",
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",
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({
name: "picker",
closeAria: t('AccessibilityClosePopup'),
closeAria: t('AccessibilityGoBack'),
header: {
title: `<div id="picker-title"></div>`,
explanation: `<div id="picker-subtitle"></div>`,
@ -334,100 +410,109 @@ export default function(obj) {
buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${t('ImagePickerDownloadAudio')}</a>`],
content: '<div id="picker-holder"></div>'
})}
${popup({
name: "download",
standalone: true,
buttonOnly: true,
classes: ["small"],
header: {
closeAria: t('AccessibilityClosePopup'),
emoji: emoji("🐱", 78, 1, 1),
title: t('TitlePopupDownload')
},
body: switcher({
<div id="popup-download-container" class="popup-from-bottom">
${popup({
name: "download",
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
items: `<a id="pd-download" class="switch full" target="_blank" href="/">${t('Download')}</a>
<div id="pd-share" class="switch full">${t('ShareURL')}</div>
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
}),
buttonText: t('PopupCloseDone')
})}
${popup({
name: "error",
standalone: true,
buttonOnly: true,
classes: ["small"],
header: {
closeAria: t('AccessibilityClosePopup'),
title: t('TitlePopupError'),
emoji: emoji("😿", 78, 1, 1),
},
body: `<div id="desc-error" class="desc-padding subtext"></div>`,
buttonText: t('ErrorPopupCloseButton')
})}
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></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="cobalt-main-box" class="center" style="visibility: hidden;">
<div id="logo">${appName}</div>
<div id="download-area">
<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>
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
</div>
<div id="bottom">
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
${switcher({
name: "audioMode",
noParent: true,
items: [{
"action": "false",
"text": `${emoji("✨")} ${t("ModeToggleAuto")}`
}, {
"action": "true",
"text": `${emoji("🎶")} ${t("ModeToggleAudio")}`
}]
})}
standalone: true,
buttonOnly: true,
classes: ["small", "glass-bkg"],
header: {
closeAria: t('AccessibilityGoBack'),
emoji: emoji("🐱", 78, 1, 1),
title: t('TitlePopupDownload')
},
body: switcher({
name: "download",
explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
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-copy" class="switch full">${t('CopyURL')}</div>`
}),
buttonText: t('PopupCloseDone')
})}
</div>
<div id="popup-error-container" class="popup-from-bottom">
${popup({
name: "error",
standalone: true,
buttonOnly: true,
classes: ["small", "glass-bkg"],
header: {
closeAria: t('AccessibilityGoBack'),
title: t('TitlePopupError'),
emoji: emoji("😿", 78, 1, 1),
},
body: `<div id="desc-error" class="desc-padding subtext"></div>`,
buttonText: t('ErrorPopupCloseButton')
})}
</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="logo">${t("AppTitleCobalt")}</div>
<div id="download-area">
<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>
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
</div>
<div id="bottom">
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
${switcher({
name: "audioMode",
noParent: true,
items: [{
action: "false",
text: `${emoji("✨")} ${t("ModeToggleAuto")}`
}, {
action: "true",
text: `${emoji("🎶")} ${t("ModeToggleAudio")}`
}]
})}
</div>
</div>
</div>
<footer id="footer" style="visibility: hidden;">
${footerButtons([{
name: "about",
type: "popup",
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
aria: t('AccessibilityOpenAbout')
}, {
name: "about",
type: "popup",
context: "donate",
text: `${emoji("💖", 22)} ${t('Donate')}`,
aria: t('AccessibilityOpenDonate')
}, {
name: "settings",
type: "popup",
text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`,
aria: t('AccessibilityOpenSettings')
}])}
</footer>
</div>
<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([{
name: "about",
type: "popup",
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
aria: t('AccessibilityOpenAbout')
}, {
name: "about",
type: "popup",
context: "donate",
text: `${emoji("💖", 22)} ${t('Donate')}`,
aria: t('AccessibilityOpenDonate')
}, {
name: "settings",
type: "popup",
text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`,
aria: t('AccessibilityOpenSettings')
}])}
</footer>
</body>
<script type="text/javascript">
const loc = {
noInternet: ` + "`" + t('ErrorNoInternet') + "`" + `,
noURLReturned: ` + "`" + t('ErrorNoUrlReturned') + "`" + `,
unknownStatus: ` + "`" + t('ErrorUnknownStatus') + "`" + `,
collapseHistory: ` + "`" + t('ChangelogPressToHide') + "`" + `,
pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `,
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
pickerDefaultExpl: ` + "`" + t(`MediaPickerExplanation${isMobile ? `Phone${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `,
};
let apiURL = '${process.env.apiURL ? process.env.apiURL.slice(0, -1) : ''}';
const loc = {
noInternet: ` + "`" + t('ErrorNoInternet') + "`" + `,
noURLReturned: ` + "`" + t('ErrorNoUrlReturned') + "`" + `,
unknownStatus: ` + "`" + t('ErrorUnknownStatus') + "`" + `,
collapseHistory: ` + "`" + t('ChangelogPressToHide') + "`" + `,
pickerDefault: ` + "`" + t('MediaPickerTitle') + "`" + `,
pickerImages: ` + "`" + t('ImagePickerTitle') + "`" + `,
pickerImagesExpl: ` + "`" + t(`ImagePickerExplanation${isMobile ? "Phone" : "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) : ''}';
</script>
<script type="text/javascript" src="cobalt.js"></script>
</html>`;
</html>
`
} catch (err) {
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,
mute: true
}
if (host === "reddit" && r.typeId === 1) responseType = 1;
break;
case "picker":

View file

@ -11,17 +11,25 @@ export default async function(obj) {
if (!("reddit_video" in data["secure_media"])) return { error: 'ErrorEmptyDownload' };
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],
audio = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
await fetch(audio, { method: "HEAD" }).then((r) => {if (Number(r.status) !== 200) audio = ''}).catch(() => {audio = ''});
let audio = false,
video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
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];
if (!audio.length > 0) return { typeId: 1, urls: video };
await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
// 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 {
typeId: 2,
type: "render",
urls: [video, audio],
urls: [video, audioFileLink],
audioFilename: `reddit_${id}_audio`,
filename: `reddit_${id}.mp4`
};

View file

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

View file

@ -3,6 +3,8 @@ import { createInterface } from "readline";
import { Cyan, Bright } from "./sub/consoleText.js";
import { execSync } from "child_process";
import { version } from "../modules/config.js";
let envPath = './.env';
let q = `${Cyan('?')} \x1b[1m`;
let ob = {};
@ -24,7 +26,7 @@ let final = () => {
}
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(

View file

@ -153,3 +153,8 @@ export function getThreads() {
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": [{
"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": {},
"expected": {
"code": 200,
@ -798,7 +798,7 @@
}
}, {
"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": {
"isAudioOnly": true
},
@ -808,7 +808,7 @@
}
}, {
"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": {
"isAudioMuted": true
},
@ -832,6 +832,14 @@
"code": 200,
"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": [{
"name": "several videos in a post (picker)",