diff --git a/README.md b/README.md index fc236e03..87ebbea2 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,14 @@ Take English or Russian localization from [this directory](https://github.com/wu - You can rephrase sentences as long as they keep the same sense. - You can add wordplays or puns if it feels natural to do so. - Even though I love cursing, keep that away from translations. +- Always check if there are issues in UI with your localization. +- There's no need to translate `ChangelogContentTitle` and `ChangelogContent`, because those are very often changed. +- Add "(in english)" to `ChangelogLastCommit` and `ChangelogLastMajor`, because those are almost always kept exclusively in English. Remove that phrase if you do translate major update changelog. - Be nice. ## TO-DO ### Services -- [x] Tumblr support - [ ] niconico support - [ ] Instagram support - [ ] SoundCloud support @@ -57,11 +59,9 @@ Take English or Russian localization from [this directory](https://github.com/wu - [ ] Add an option to keep watermark on TikTok videos ### Other +- [ ] Add support for emoji in localization - [ ] Language picker in settings -- [ ] Make switch buttons in settings selectable with keyboard -- [ ] Option to save audios in formats other than original - [ ] Make cobalt fully PWA compatible (add a service worker) -- [ ] Make page rendering module more versatile ## Host an instance yourself Code might be a little messy, but I do my best to improve it with every commit. @@ -93,5 +93,8 @@ Setup script installs all needed `npm` dependencies, but you have to install `No ## Disclaimer This is my passion project, so update scheduele depends solely on my motivation. Don't expect any consistency in that. +## Third party stuff +[Fluent Emoji](https://github.com/microsoft/fluentui-emoji) by Microsoft. + ## License cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE). diff --git a/package.json b/package.json index e202ee43..4792ed84 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "2.2.9", + "version": "3.0", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/cobalt.js b/src/cobalt.js index c5cd68e3..10dd2093 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -8,11 +8,11 @@ import rateLimit from "express-rate-limit"; import { shortCommit } from "./modules/sub/currentCommit.js"; import { appName, genericUserAgent, version, internetExplorerRedirect } from "./modules/config.js"; import { getJSON } from "./modules/api.js"; -import renderPage from "./modules/pageRender.js"; +import renderPage from "./modules/pageRender/page.js"; import { apiJSON, languageCode } from "./modules/sub/utils.js"; import { Bright, Cyan } from "./modules/sub/consoleText.js"; import stream from "./modules/stream/stream.js"; -import loc, { loadLoc } from "./localization/manager.js"; +import loc from "./localization/manager.js"; import { buildFront } from "./modules/build.js"; const commitHash = shortCommit(); @@ -65,8 +65,10 @@ if (fs.existsSync('./.env')) { req.query.url.trim(), req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip, languageCode(req), - req.query.format ? req.query.format.slice(0, 5) : "mp4", - req.query.quality ? req.query.quality.slice(0, 3) : "max" + req.query.format ? req.query.format.slice(0, 5) : "webm", + req.query.quality ? req.query.quality.slice(0, 3) : "max", + req.query.audioFormat ? req.query.audioFormat.slice(0, 4) : false, + req.query.audio ? true : false ) res.status(j.status).json(j.body); } else { @@ -79,7 +81,7 @@ if (fs.existsSync('./.env')) { res.status(200).json({ "status": "continue" }); } else if (req.query.t) { let ip = req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip - stream(res, ip, req.query.t, req.query.h, req.query.e); + stream(res, ip, req.query.t, req.query.h, req.query.e, languageCode(req)); } else { let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorNoStreamID') }) res.status(j.status).json(j.body); diff --git a/src/config.json b/src/config.json index 88bbdf7b..53a49ea6 100644 --- a/src/config.json +++ b/src/config.json @@ -13,21 +13,28 @@ "new": "https://vivaldi.com/" }, "donations": { - "ethereum": "0x4B4cF23051c78c7A7E0eA09d39099621c46bc302", - "bitcoin": "bc1q64amsn0wd60urem3jkhpywed8q8kqwssw6ta5j", - "litecoin": "ltc1qvp0xhrk2m7pa6p6z844qcslfyxv4p3vf95rhna", - "bitcoin cash": "bitcoincash:qph0d7d02mvg5xxqjwjv5ahjx2254yx5kv0zfg0xsj", - "monero": "4B1SNB6s8Pq1hxjNeKPEe8Qa8EP3zdL16Sqsa7QDoJcUecKQzEj9BMxWnEnTGu12doKLJBKRDUqnn6V9qfSdXpXi3Nw5Uod" + "crypto": { + "ethereum": "0x4B4cF23051c78c7A7E0eA09d39099621c46bc302", + "bitcoin": "bc1q64amsn0wd60urem3jkhpywed8q8kqwssw6ta5j", + "litecoin": "ltc1qvp0xhrk2m7pa6p6z844qcslfyxv4p3vf95rhna", + "bitcoin cash": "bitcoincash:qph0d7d02mvg5xxqjwjv5ahjx2254yx5kv0zfg0xsj", + "monero": "4B1SNB6s8Pq1hxjNeKPEe8Qa8EP3zdL16Sqsa7QDoJcUecKQzEj9BMxWnEnTGu12doKLJBKRDUqnn6V9qfSdXpXi3Nw5Uod" + }, + "other": { + "boosty": "https://boosty.to/wukko" + } }, "quality": { "hig": "1080", "mid": "720", "low": "480" }, + "supportedAudio": ["mp3", "ogg", "opus"], "ffmpegArgs": { "webm": ["-c:v", "copy", "-c:a", "copy"], "mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "frag_keyframe+empty_moov"], - "bst": ["-c:a", "copy"], - "mp3": ["-ar", "48000", "-ac", "2", "-b:a", "320k"] + "copy": ["-c:a", "copy"], + "audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"], + "m4a": ["-movflags", "frag_keyframe+empty_moov"] } } diff --git a/src/front/cobalt.css b/src/front/cobalt.css index bd79714c..10ea06ed 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -8,7 +8,8 @@ @media (prefers-color-scheme: dark) { :root { --accent: rgb(225, 225, 225); - --accent-hover: rgb(20, 20, 20); + --accent-hover: rgb(25, 25, 25); + --accent-button-bg: rgb(20, 20, 20); --accent-press: rgb(10, 10, 10); --accent-unhover: rgb(100, 100, 100); --accent-unhover-2: rgb(110, 110, 110); @@ -18,8 +19,9 @@ @media (prefers-color-scheme: light) { :root { --accent: rgb(25, 25, 25); - --accent-hover: rgb(230 230 230); - --accent-press: rgb(240 240 240); + --accent-hover: rgb(225, 225, 225); + --accent-button-bg: rgb(230, 230, 230); + --accent-press: rgb(240, 240, 240); --accent-unhover: rgb(190, 190, 190); --accent-unhover-2: rgb(110, 110, 110); --background: rgb(255, 255, 255); @@ -27,7 +29,8 @@ } [data-theme="dark"] { --accent: rgb(225, 225, 225); - --accent-hover: rgb(20, 20, 20); + --accent-hover: rgb(25, 25, 25); + --accent-button-bg: rgb(20, 20, 20); --accent-press: rgb(10, 10, 10); --accent-unhover: rgb(100, 100, 100); --accent-unhover-2: rgb(110, 110, 110); @@ -35,8 +38,9 @@ } [data-theme="light"] { --accent: rgb(25, 25, 25); - --accent-hover: rgb(230 230 230); - --accent-press: rgb(240 240 240); + --accent-hover: rgb(225, 225, 225); + --accent-button-bg: rgb(230, 230, 230); + --accent-press: rgb(240, 240, 240); --accent-unhover: rgb(190, 190, 190); --accent-unhover-2: rgb(110, 110, 110); --background: rgb(255, 255, 255); @@ -56,6 +60,7 @@ body { a { color: var(--accent); text-decoration: none; + user-select: none; } ::placeholder { color: var(--accent-unhover-2); @@ -79,13 +84,13 @@ a { width: 15px; height: 15px; border: var(--border-15); - background-color: var(--background); + background-color: var(--accent-button-bg); display: block; z-index: 5; position: relative; } [type="checkbox"]:checked::before { - box-shadow: inset 0 0 0 0.2rem var(--background); + box-shadow: inset 0 0 0 0.2rem var(--accent-button-bg); background-color: var(--accent); } button { @@ -120,6 +125,7 @@ button:active, .text-to-copy:active { background: var(--accent-press); cursor: pointer; + transform: scale(0.95) } input[type="checkbox"] { cursor: pointer; @@ -202,12 +208,19 @@ input[type="checkbox"] { text-align: center; width: 90%; } +#footer-buttons { + display: inline-flex; + align-items: center; +} .footer-button { cursor: pointer; color: var(--accent-unhover-2); - border: 0.15rem var(--accent-unhover-2) solid; + border: 0.15rem solid var(--accent-unhover-2); padding: 0.4rem 0.8rem 0.5rem; - margin-bottom: 0.5rem; + margin: 0.4rem; + display: flex; + align-content: center; + align-items: center; } .footer-button:hover { color: var(--accent); @@ -252,31 +265,38 @@ input[type="checkbox"] { .scrollable .bottom-link { padding-bottom: 2rem; } +.changelog-subtitle { + font-size: 1.1rem; + padding-bottom: 0.7rem; +} .nowrap { white-space: nowrap; } -.about-padding { +.no-top-padding { + padding-top: 0!important; +} +.desc-padding { padding-bottom: 1.5rem; } -.popup-subtitle { +#popup-subtitle { font-size: 1.1rem; - padding-bottom: 0.5rem; + padding-bottom: 1rem; } -.little-subtitle { - font-size: 1.05rem; -} -.popup-desc { +#popup-desc, +#desc-error { width: 100%; text-align: left; float: left; line-height: 1.7rem; } -.popup-title { +#popup-title { font-size: 1.5rem; margin-bottom: 0.5rem; line-height: 1.85em; + display: flex; + align-items: center; } -.popup-footer { +#popup-footer { bottom: 0; position: fixed; margin-bottom: 1.5rem; @@ -290,25 +310,25 @@ input[type="checkbox"] { border-top: 0.05rem solid var(--accent-unhover-2); padding-top: 0.4rem; } -.popup-above-title { +#popup-above-title { color: var(--accent-unhover-2); font-size: 0.8rem; } -.popup-content { +#popup-content { overflow-x: hidden; overflow-y: auto; height: var(--without-padding); scrollbar-width: none; } -.popup-header { +#popup-header { position: relative; background: var(--background); z-index: 999; } -.popup-content.with-footer { +#popup-content.with-footer { margin-bottom: 3rem; } -#close { +#popup-close { cursor: pointer; float: right; right: 0rem; @@ -317,13 +337,14 @@ input[type="checkbox"] { .settings-category { padding-bottom: 1.2rem; } -.title { +.category-title { width: 100%; text-align: left; line-height: 1.7rem; color: var(--accent-unhover-2); border-bottom: 0.05rem solid var(--accent-unhover-2); padding-bottom: 0.25rem; + margin-bottom: 1rem; } .bottom-margin { margin-bottom: 1rem; @@ -336,9 +357,9 @@ input[type="checkbox"] { align-content: center; padding: 0.6rem; padding-right: 1rem; - border: var(--border-10); width: auto; margin: 0 0.5rem 0.5rem 0; + background: var(--accent-button-bg); } .checkbox-label { line-height: 1.3rem; @@ -352,6 +373,8 @@ input[type="checkbox"] { line-height: 1.7rem; padding-bottom: 0.4rem; color: var(--accent); +} +.subtitle.extra { margin-top: 1rem; } .small-padding .subtitle { @@ -362,30 +385,20 @@ input[type="checkbox"] { width: 100%; font-size: 0.8rem; text-align: left; - line-height: 1.3rem; + line-height: 1.3rem!important; color: var(--accent-unhover-2); } .switch { - border-top: var(--border-10); - border-bottom: var(--border-10); padding: 0.8rem; width: 100%; text-align: center; color: var(--accent); - background: var(--background); - display: grid; + background: var(--accent-button-bg); + display: flex; + justify-content: center; align-items: center; cursor: pointer; } -.switch.full { - border: var(--border-10); -} -.switch.left { - border-left: var(--border-10); -} -.switch.right { - border-right: var(--border-10); -} .switch.space-right { margin-right: 1rem } @@ -399,6 +412,13 @@ input[type="checkbox"] { width: auto; flex-direction: row; flex-wrap: nowrap; + overflow-x: scroll; +} +.autowidth { + width: auto; +} +.bottom-space { + margin-bottom: 2rem; } .text-to-copy { user-select: text; @@ -406,6 +426,26 @@ input[type="checkbox"] { padding: 1rem; overflow: auto; } +#close-bottom { + width: 18%; + margin-left: 1rem; + background: var(--background); + border: var(--border-15); + color: var(--accent); + padding: 0.3rem 0.75rem 0.5rem; +} +.popup-tab-content { + display: none; +} +#popup-tabs { + z-index: 999; + bottom: 0; + position: relative; + width: 100%; +} +.emoji { + margin-right: 0.4rem; +} /* adapt the page according to screen size */ @media screen and (min-width: 2300px) { html { @@ -430,7 +470,15 @@ input[type="checkbox"] { width: 40%; } } -@media screen and (max-width: 1024px) { +@media screen and (max-width: 1100px) { + #cobalt-main-box { + width: 70%; + } + .popup { + width: 50%; + } +} +@media screen and (max-width: 1025px) { #cobalt-main-box { width: 75%; } @@ -458,7 +506,7 @@ input[type="checkbox"] { text-align: center; } #cobalt-main-box { - width: 80%; + width: 85%; display: flex; border: none; padding: 0; @@ -473,7 +521,13 @@ input[type="checkbox"] { padding-bottom: 2rem; } } -@media screen and (max-width: 524px) { +@media screen and (max-width: 475px) { + .tab { + font-size: 0; + } + .tab .emoji { + margin-right: 0; + } #logo-area { padding-right: 0; padding-top: 0; diff --git a/src/front/cobalt.js b/src/front/cobalt.js index f6dff56d..3bea8f6a 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,11 +1,15 @@ let isIOS = navigator.userAgent.toLowerCase().match("iphone os"); +let version = 3 + let switchers = { "theme": ["auto", "light", "dark"], - "youtubeFormat": ["webm", "mp4", "audio"], - "quality": ["max", "hig", "mid", "low"] + "ytFormat": ["webm", "mp4"], + "quality": ["max", "hig", "mid", "low"], + "audioFormat": ["best", "mp3", "ogg", "opus"] } -let exceptions = { - "youtubeFormat": "mp4" +let exceptions = { // fuck you apple + "ytFormat": "mp4", + "audioFormat": "mp3" } function eid(id) { @@ -20,6 +24,9 @@ function disable(id) { function vis(state) { return (state === 1) ? "visible" : "hidden"; } +function opposite(state) { + return state == "true" ? "false" : "true"; +} function changeDownloadButton(action, text) { switch (action) { case 0: @@ -70,13 +77,41 @@ function detectColorScheme() { } document.documentElement.setAttribute("data-theme", theme); } +function changeTab(evnt, tabId, tabClass) { + let tabcontent = document.getElementsByClassName(`tab-content-${tabClass}`); + let tablinks = document.getElementsByClassName(`tab-${tabClass}`); + for (let i = 0; i < tabcontent.length; i++) { + tabcontent[i].style.display = "none"; + } + for (let i = 0; i < tablinks.length; i++) { + tablinks[i].dataset.enabled = "false"; + } + eid(tabId).style.display = "block"; + evnt.currentTarget.dataset.enabled = "true"; +} +function hideAllPopups() { + let filter = document.getElementsByClassName('popup'); + for (let i = 0; i < filter.length; i++) { + filter[i].style.visibility = "hidden"; + } + eid("popup-backdrop").style.visibility = "hidden"; +} function popup(type, action, text) { eid("popup-backdrop").style.visibility = vis(action); switch (type) { case "about": + let tabId = text ? text : "changelog"; + if (tabId == "changelog") { + localStorage.setItem("changelogStatus", version) + } + eid(`tab-button-${type}-${tabId}`).click(); eid("popup-about").style.visibility = vis(action); if (!localStorage.getItem("seenAbout")) localStorage.setItem("seenAbout", "true"); break; + case "settings": + eid(`tab-button-${type}-video`).click(); + eid("popup-settings").style.visibility = vis(action); + break; case "error": eid("desc-error").innerHTML = text; eid("popup-error").style.visibility = vis(action); @@ -93,17 +128,17 @@ function popup(type, action, text) { break; } } -function changeSwitcher(li, b, u) { - if (u) localStorage.setItem(li, b); +function changeSwitcher(li, b) { if (b) { + localStorage.setItem(li, b); for (i in switchers[li]) { (switchers[li][i] == b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`) } if (li == "theme") detectColorScheme(); } else { let pref = switchers[li][0]; - if (isIOS && exceptions[li]) pref = exceptions[li]; localStorage.setItem(li, pref); + if (isIOS && exceptions[li]) pref = exceptions[li]; for (i in switchers[li]) { (switchers[li][i] == pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`) } @@ -132,18 +167,49 @@ function loadSettings() { if (localStorage.getItem("downloadPopup") == "true" && !isIOS) { eid("downloadPopup").checked = true; } - changeSwitcher("theme", localStorage.getItem("theme")) - changeSwitcher("youtubeFormat", localStorage.getItem("youtubeFormat")) - changeSwitcher("quality", localStorage.getItem("quality")) + if (!localStorage.getItem("audioMode")) { + toggle("audioMode") + } + updateToggle("audioMode", localStorage.getItem("audioMode")) + for (let i in switchers) { + changeSwitcher(i, localStorage.getItem(i)) + } +} +function checkbox(action) { + if (eid(action).checked) { + localStorage.setItem(action, "true"); + if (action == "alwaysVisibleButton") button(); + } else { + localStorage.setItem(action, "false"); + if (action == "alwaysVisibleButton") button(); + } +} +function toggle(toggle) { + let state = localStorage.getItem(toggle); + if (state) { + localStorage.setItem(toggle, opposite(state)) + } else { + localStorage.setItem(toggle, "false") + } + updateToggle(toggle, localStorage.getItem(toggle)) +} +function updateToggle(toggle, state) { + switch(state) { + case "true": + eid(toggle).innerHTML = loc.toggleAudio; + break; + case "false": + eid(toggle).innerHTML = loc.toggleDefault; + break; + } } async function download(url) { changeDownloadButton(2, '...'); eid("url-input-area").disabled = true; - let format = ''; - if (url.includes("youtube.com/") || url.includes("/youtu.be/")) { - format = `&format=${localStorage.getItem("youtubeFormat")}` - } - fetch(`/api/json?quality=${localStorage.getItem("quality")}${format}&url=${encodeURIComponent(url)}`).then(async (response) => { + let audioMode = localStorage.getItem("audioMode"); + let format = (url.includes("youtube.com/") && audioMode == "false" || url.includes("/youtu.be/") && audioMode == "false") ? `&format=${localStorage.getItem("ytFormat")}` : ''; + let mode = (localStorage.getItem("audioMode") == "true") ? `audio=true` : `quality=${localStorage.getItem("quality")}`; + fetch(`/api/json?audioFormat=${localStorage.getItem("audioFormat")}&${mode}${format}&url=${encodeURIComponent(url)}`).then(async (response) => { let j = await response.json(); if (j.status != "error" && j.status != "rate-limit") { if (j.url) { @@ -198,11 +264,16 @@ window.onload = function () { eid("cobalt-main-box").style.visibility = 'visible'; eid("footer").style.visibility = 'visible'; eid("url-input-area").value = ""; - if (!localStorage.getItem("seenAbout")) popup('about', 1); + if (!localStorage.getItem("seenAbout")) { + popup('about', 1, "about"); + } else if (localStorage.getItem("changelogStatus") != `${version}` && localStorage.getItem("disableChangelog") != "true") { + popup('about', 1, "changelog"); + } if (isIOS) localStorage.setItem("downloadPopup", "true"); } eid("url-input-area").addEventListener("keyup", (event) => { - if (event.key === 'Enter') { - eid("download-button").click(); - } + if (event.key === 'Enter') eid("download-button").click(); }) +document.onkeydown = function(event) { + if (event.key === 'Escape') hideAllPopups(); +}; \ No newline at end of file diff --git a/src/front/emoji/clapper_board.svg b/src/front/emoji/clapper_board.svg new file mode 100644 index 00000000..8bcf482b --- /dev/null +++ b/src/front/emoji/clapper_board.svg @@ -0,0 +1,13 @@ + diff --git a/src/front/emoji/dragon_face.svg b/src/front/emoji/dragon_face.svg new file mode 100644 index 00000000..6225b42c --- /dev/null +++ b/src/front/emoji/dragon_face.svg @@ -0,0 +1,11 @@ + diff --git a/src/front/emoji/gear.svg b/src/front/emoji/gear.svg new file mode 100644 index 00000000..8351a33a --- /dev/null +++ b/src/front/emoji/gear.svg @@ -0,0 +1,5 @@ + diff --git a/src/front/emoji/magic_wand.svg b/src/front/emoji/magic_wand.svg new file mode 100644 index 00000000..9df52acf --- /dev/null +++ b/src/front/emoji/magic_wand.svg @@ -0,0 +1,5 @@ + diff --git a/src/front/emoji/money_bag.svg b/src/front/emoji/money_bag.svg new file mode 100644 index 00000000..e5059e2c --- /dev/null +++ b/src/front/emoji/money_bag.svg @@ -0,0 +1,4 @@ + diff --git a/src/front/emoji/money_with_wings.svg b/src/front/emoji/money_with_wings.svg new file mode 100644 index 00000000..56d0cb0c --- /dev/null +++ b/src/front/emoji/money_with_wings.svg @@ -0,0 +1,12 @@ + diff --git a/src/front/emoji/musical_notes.svg b/src/front/emoji/musical_notes.svg new file mode 100644 index 00000000..f66414f0 --- /dev/null +++ b/src/front/emoji/musical_notes.svg @@ -0,0 +1,5 @@ + diff --git a/src/front/emoji/party_popper.svg b/src/front/emoji/party_popper.svg new file mode 100644 index 00000000..6d69b12f --- /dev/null +++ b/src/front/emoji/party_popper.svg @@ -0,0 +1,15 @@ + diff --git a/src/front/emoji/pinata.svg b/src/front/emoji/pinata.svg new file mode 100644 index 00000000..cf260701 --- /dev/null +++ b/src/front/emoji/pinata.svg @@ -0,0 +1,16 @@ + diff --git a/src/front/emoji/red_question_mark.svg b/src/front/emoji/red_question_mark.svg new file mode 100644 index 00000000..0227702f --- /dev/null +++ b/src/front/emoji/red_question_mark.svg @@ -0,0 +1,4 @@ + diff --git a/src/front/emoji/sparkles.svg b/src/front/emoji/sparkles.svg new file mode 100644 index 00000000..ddce2e7d --- /dev/null +++ b/src/front/emoji/sparkles.svg @@ -0,0 +1,5 @@ + diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 0b5d13bf..1de66f4b 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -2,9 +2,11 @@ "name": "english", "code": "en", "substrings": { - "ContactLink": "let me know" + "ContactLink": "let me know" }, "strings": { + "ChangelogContentTitle": "everything what you've been waiting for. welcome to cobalt 3.0 :)", + "ChangelogContent": "follow cobalt's twitter account for polls, updates, and more: @justusecobalt\n\nstuff that you can notice:\n\n- you can now download audio from any supported service, in any format that you set in settings (+). yes, that includes mp3, which you all have been waiting for :D\n- it's now easier to switch between download modes (just a single toggle on the bottom).\n- your youtube download format has been reset, sorry, but that was required to implement all audio downloads.\n- default download format for youtube videos on all platforms is now webm. except for ios.\n\n- cobalt now has emoji, just to spice up the black and white ui. all of them have been tuned to look the best in both themes. isn't it cool?\n- about, changelog, and donation popups have been merged into just one, for covnenience.\n- changelog got a huge upgrade (as you can see), and now there are both major changes and latest commit info, just so commits can finally go back to being batshit insane.\n- changelog popup appears on every major update, but you can disable it in settings, if you want to.\n- changelog now opens by default when pressing \"?\" button. i don't think anyone reads \"about\" as often.\n- settings (+) have been split into three tabs, also for convenience and ease of use.\n\n- added support for donation links. you can now donate through boosty, not only via crypto :D\n- donate popup has been rearranged and tuned just a tiny bit. \n\n- you can now click away from any popup by pressing the void behind it.\n- you can also press \"escape\" key on keyboard to close any popup.\n\n- switchers and buttons are now way easier on eye. white border is gone from where it's unneeded.\n- buttons are now very satisfying to press.\n- switchers are scrollable if there's not enough space to fit all contents on screen.\n- scaling is now even better than before.\n\ninternal stuff:\n\n- frontend won't send video related stuff if audio mode is on.\n- matching has, yet again, gone through mitosis, and is now probably the cleanest it can get.\n- page rendering is now modular, something like what frameworks have but way lighter. this makes adding new features WAY easier.\n- removed some stuff that didn't make sense (like storing language of stream request).\n- cleaned up insides of cobalt, of course.\n- almost all links now open in new tab, just like they should have from the very beginning.\n\nknown issues:\n- impossible to download audio from vk. i'll try to fix it in the next update.\n- headers are not sticky in tabbed popups. maybe this is a good thing, i'll think about it.\n\nif you ever notice any issues, make sure to report them on github. your report doesn't have to sound professional, just do your best to describe the issue.", "LinkInput": "paste the link here", "AboutSummary": "{appName} is your go-to place for social media downloads. zero ads, trackers, or any other creepy bullshit attached. simply paste a share link and you're ready to rock!", "AboutSupportedServices": "currently supported services:", @@ -35,7 +37,7 @@ "ErrorCorruptedStream": "this download is unfortunately corrupted. try again or try a different format and resolution.", "ErrorNoInternet": "there's no internet or {appName} api is down. check your connection and try again.", "ErrorCantConnectToServiceAPI": "i couldn't connect to {s} api. seems like either {s} is down or {appName} server ip got blocked. try again later.", - "ErrorEmptyDownload": "there's no media content to download. try something else!", + "ErrorEmptyDownload": "there's nothing to download. try something else!", "ErrorLiveVideo": "i can't download a live video. wait for stream to finish and try again.", "ErrorNoStreamID": "there's no such streamId.", "ErrorNoType": "there's no such expected response type.", @@ -53,15 +55,15 @@ "SettingsQualitySwitchMedium": "medium\n", "SettingsQualitySwitchLow": "low\n", "SettingsQualitySwitchLowest": "lowest", - "SettingsFormatSwitchAudio": "audio only", + "SettingsFormatSwitchAudio": "only audio", "SettingsKeepDownloadButton": "keep >> visible", "AccessibilityKeepDownloadButton": "keep the download button always visible", "SettingsEnableDownloadPopup": "ask for a way to save", "AccessibilityEnableDownloadPopup": "ask what to do with downloads", - "SettingsFormatDescription": "select webm if you need max quality available. webm videos are usually higher quality but ios devices can't play them natively. all \"audio only\" downloads are max quality.", - "SettingsQualityDescription": "if selected resolution isn't available, closest one gets picked instead. if you want to post a youtube video on twitter, select 720p. twitter likes videos like that way more.", - "DonateSubtitle": "it's hard to pay for hosting right now", - "DonateDescription": "i don't like crypto how it is right now, but it's currently the only way for me to pay for anything abroad. mastercard/visa cards and services similar to paypal are no longer an option.", + "SettingsFormatDescription": "select webm if you need max quality available. webm videos are usually higher quality but ios devices can't play them natively.", + "SettingsQualityDescription": "if selected resolution isn't available, closest one gets picked instead. if you want to post a youtube video on twitter, then select a combination of mp4 and 720p. twitter likes videos like that way more.", + "DonateSubtitle": "help me pay for hosting cobalt", + "DonateDescription": "i don't really like crypto in its current state, but it's the only reliable way for me to receive money and pay for anything abroad.", "LinkGitHubIssues": ">> report issues and check out the source code on github", "LinkGitHubChanges": ">> see previous changes and contribute on github", "LinkDonateContact": ">> let me know if currency you want to donate isn't listed", @@ -71,6 +73,21 @@ "DownloadPopupWayToSave": "pick a way to save", "ClickToCopy": "click to copy", "Download": "download", - "CopyURL": "copy url" + "CopyURL": "copy url", + "AboutTab": "about", + "ChangelogTab": "changelog", + "DonationsTab": "donations", + "SettingsVideoTab": "video", + "SettingsAudioTab": "audio", + "SettingsOtherTab": "other", + "ChangelogLastCommit": "last commit", + "ChangelogLastMajor": "last major update", + "ModeToggleDefault": "smart mode", + "AccessibilityModeToggle": "toggle download mode", + "DonateLinksDescription": "donation links open in a new tab. this is the best way to donate money, if you want me to receive it directly.", + "SettingsAudioFormatBest": "best", + "SettingsAudioFormatDescription": "when best format is selected, you get audio in best quality available, because audio is kept in its original format. if you select anything other than that, you'll get a slightly compressed file.", + "Keyphrase": "save what you love", + "SettingsDisableChangelogOnUpdate": "don't show changelog after major updates" } } diff --git a/src/localization/languages/es.json b/src/localization/languages/es.json index 0e4f55c8..1f5c4031 100644 --- a/src/localization/languages/es.json +++ b/src/localization/languages/es.json @@ -2,7 +2,7 @@ "name": "español", "code": "es", "substrings": { - "ContactLink": "hazme saber" + "ContactLink": "hazme saber" }, "strings": { "LinkInput": "pega tu enlace aquí", @@ -58,8 +58,8 @@ "AccessibilityKeepDownloadButton": "mantener el botón de descarga siempre visible", "SettingsEnableDownloadPopup": "pregunta por la forma de guardar", "AccessibilityEnableDownloadPopup": "preguntar qué hacer con las descargas", - "SettingsFormatDescription": "selecciona webm si necesitas la máxima calidad disponible. los videos webm suelen ser de mayor calidad, pero los dispositivos ios no pueden reproducirlos de forma nativa. todas las descargas \"solo audio\" son de máxima calidad.", - "SettingsQualityDescription": "si la resolución seleccionada no está disponible, se elige la más cercana en su lugar. Si quieres publicar un video de youtube a twitter, selecciona 720p. A twitter le gustan más los videos así.", + "SettingsFormatDescription": "selecciona webm si necesitas la máxima calidad disponible. los videos webm suelen ser de mayor calidad, pero los dispositivos ios no pueden reproducirlos de forma nativa.", + "SettingsQualityDescription": "si la resolución seleccionada no está disponible, se elige la más cercana en su lugar. Si quieres publicar un video de youtube a twitter, seleccionar combinación de mp4 y 720p. A twitter le gustan más los videos así.", "DonateSubtitle": "pagar por hosting es un poco complicado ahora mismo", "DonateDescription": "no me gusta cómo está crypto ahora mismo, pero es la única forma que tengo para pagar cualquier cosa en línea. las tarjetas mastercard/visa y los servicios similares a paypal ya no están disponibles.", "LinkGitHubIssues": ">> informa sobre problemas y consulta el código fuente en github", diff --git a/src/localization/languages/fr.json b/src/localization/languages/fr.json index 50d08972..887455de 100644 --- a/src/localization/languages/fr.json +++ b/src/localization/languages/fr.json @@ -2,7 +2,7 @@ "name": "français", "code": "fr", "substrings": { - "ContactLink": "fais-moi signe" + "ContactLink": "fais-moi signe" }, "strings": { "LinkInput": "copie ton lien ici", @@ -58,8 +58,8 @@ "AccessibilityKeepDownloadButton": "garder le bouton télécharger visible en tout temps", "SettingsEnableDownloadPopup": "demander comment sauvegarder", "AccessibilityEnableDownloadPopup": "demander comment sauvegarder", - "SettingsFormatDescription": "sélectionne webm si tu veux la meilleure qualité possible. les vidéos webm sont habituellement de plus haute qualité, mais les appareils ios ne peuvent pas les jouer par défaut. tous les téléchargements «audio seulement» sont de qualité maximum.", - "SettingsQualityDescription": "si la résolution sélectionnée n'est pas disponible, celle la plus proche est choisie à la place. si tu veux envoyer une vidéo youtube sur twitter, sélectionne 720p. twitter préfère les vidéos dans ce format.", + "SettingsFormatDescription": "sélectionne webm si tu veux la meilleure qualité possible. les vidéos webm sont habituellement de plus haute qualité, mais les appareils ios ne peuvent pas les jouer par défaut.", + "SettingsQualityDescription": "si la résolution sélectionnée n'est pas disponible, celle la plus proche est choisie à la place. si tu veux envoyer une vidéo youtube sur twitter, sélectionne combinaison de mp4 et 720p. twitter préfère les vidéos dans ce format.", "DonateSubtitle": "parce que c'est difficile de payer pour l'hébergement", "DonateDescription": "je n'aime pas l'état de la crypto présentement, mais c'est présentement la seule manière pour moi de payer à l'international. les cartes mastercard/visa et services similaires à paypal ne sont présentement plus une option.", "LinkGitHubIssues": ">> signale des problèmes et examine le code source sur github", diff --git a/src/localization/languages/id.json b/src/localization/languages/id.json index e49c71cf..5f6b618f 100644 --- a/src/localization/languages/id.json +++ b/src/localization/languages/id.json @@ -2,7 +2,7 @@ "name": "indonesia", "code": "id", "substrings": { - "ContactLink": "beri tau saya" + "ContactLink": "beri tau saya" }, "strings": { "LinkInput": "tempel link kamu disini", @@ -58,8 +58,8 @@ "AccessibilityKeepDownloadButton": "biarkan tombol download selalu kelihatan", "SettingsEnableDownloadPopup": "tanya cara untuk simpan", "AccessibilityEnableDownloadPopup": "tanyakan apa yang dilakukan dengan file yang baru didownload", - "SettingsFormatDescription": "pilih webm kalo kamu butuh kualitas tertinggi. kualitas video webm biasanya lebih tinggi tapi perangkat ios tidak bisa menjalankannya secara native. semua download \"audio saja\" sudah kualitas tertinggi", - "SettingsQualityDescription": "jika resolusi yang dipilih tidak tersedia, resolusi yang dipilih akan diganti dengan resolusi tersedia yang terdekat. kalo kamu mau kirim video youtube di twitter, pilih 720p saja. twitter lebih suka video yang 720p.", + "SettingsFormatDescription": "pilih webm kalo kamu butuh kualitas tertinggi. kualitas video webm biasanya lebih tinggi tapi perangkat ios tidak bisa menjalankannya secara native.", + "SettingsQualityDescription": "jika resolusi yang dipilih tidak tersedia, resolusi yang dipilih akan diganti dengan resolusi tersedia yang terdekat. kalo kamu mau kirim video youtube di twitter, pilih kombinasi mp4 dan 720p. twitter lebih suka video seperti itu.", "DonateSubtitle": "sekarang lagi susah untuk bayar hosting bagi saya", "DonateDescription": "saya tidak suka crypto dengan keadaannya sekarang, tetapi saat ini itu adalah satu-satunya cara bagi saya untuk membayar apa pun yang di luar negeri. kartu mastercard/visa dan layanan yang mirip seperti paypal tidak lagi menjadi pilihan.", "LinkGitHubIssues": ">> laporkan masalah atau liat source code di github", diff --git a/src/localization/languages/pl.json b/src/localization/languages/pl.json index bc83c378..855ce14b 100644 --- a/src/localization/languages/pl.json +++ b/src/localization/languages/pl.json @@ -2,7 +2,7 @@ "name": "polish", "code": "pl", "substrings": { - "ContactLink": "daj mi znać" + "ContactLink": "daj mi znać" }, "strings": { "LinkInput": "wklej link tutaj", @@ -58,8 +58,8 @@ "AccessibilityKeepDownloadButton": "pozostaw przycisk pobierania zawsze widoczny", "SettingsEnableDownloadPopup": "pytaj o sposób zapisu", "AccessibilityEnableDownloadPopup": "pytaj co zrobić z pobranymi plikami", - "SettingsFormatDescription": "wybierz webm jeżeli potrzebujesz najwyższej możliwej jakości. filmy webm są zwykle wyższej jakości ale urządzenia działające na systemie ios nie odtwarzają ich natywnie. wszystkie pliki \"tylko audio\" są maksymalnej jakości.", - "SettingsQualityDescription": "jeżeli wybrana rozdzielczość nie będzie dostępna, zostanie wybrana najbliższa pasująca. jeżeli chcesz wrzucić film z youtube'a na twittera, wybierz 720p. twitter bardziej lubi takie filmy.", + "SettingsFormatDescription": "wybierz webm jeżeli potrzebujesz najwyższej możliwej jakości. filmy webm są zwykle wyższej jakości ale urządzenia działające na systemie ios nie odtwarzają ich natywnie.", + "SettingsQualityDescription": "jeżeli wybrana rozdzielczość nie będzie dostępna, zostanie wybrana najbliższa pasująca. jeżeli chcesz wrzucić film z youtube'a na twittera, wybierz połączenie mp4 i 720p. twitter bardziej lubi takie filmy.", "DonateSubtitle": "ciężko się teraz płaci za hosting", "DonateDescription": "nie podoba mi się stan w jakim są teraz kryptowaluty, ale na razie jest to dla mnie jedyny sposób żeby płacić za coś za granicą. karty mastercard/visa i usługi takie jak paypal nie są już dostępną opcją.", "LinkGitHubIssues": ">> zgłoś problem lub zobacz kod źródłowy na githubie", diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index 0095e2e8..7ee8310f 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -2,7 +2,7 @@ "name": "русский", "code": "ru", "substrings": { - "ContactLink": "напиши об этом" + "ContactLink": "напиши об этом" }, "strings": { "LinkInput": "вставь ссылку сюда", @@ -58,12 +58,12 @@ "AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране", "SettingsEnableDownloadPopup": "спрашивать, как сохранять", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", - "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений. все загрузки \"только аудио\" всегда максимального качества.", - "SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай 720p. такие видео твиттер обычно воспринимает намного лучше.", - "DonateSubtitle": "сейчас намного сложнее оплачивать хостинг", - "DonateDescription": "я ненавижу крипто, но у меня нет возможности платить любым другим способом.", + "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.", + "SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. такие видео твиттер обычно воспринимает намного лучше.", + "DonateSubtitle": "помоги мне платить за хостинг", + "DonateDescription": "я не люблю крипто в его текущем состоянии, но у меня нет другого надёжного способа оплаты хостинга.", "LinkGitHubIssues": ">> сообщай о проблемах и смотри исходный код на github", - "LinkGitHubChanges": ">> посмотреть предыдущие изменения на github", + "LinkGitHubChanges": ">> смотри предыдущие изменения на github", "LinkDonateContact": ">> напиши мне, если в этом списке нет подходящей валюты", "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.", "DownloadPopupDescriptionIOS": "так как у тебя устройство на ios, тебе нужно зажать кнопку \"скачать\" и выбрать что-то похожее на \"сохранить в галерею\" в появившемся окне.", @@ -71,6 +71,21 @@ "DownloadPopupWayToSave": "выбери, как сохранить", "ClickToCopy": "нажми, чтобы скопировать", "Download": "скачать", - "CopyURL": "скопировать ссылку" + "CopyURL": "скопировать ссылку", + "AboutTab": "о cobalt", + "ChangelogTab": "изменения", + "DonationsTab": "донаты", + "SettingsVideoTab": "видео", + "SettingsAudioTab": "аудио", + "SettingsOtherTab": "другое", + "ChangelogLastCommit": "последний коммит (на английском)", + "ChangelogLastMajor": "последнее обновление (на английском)", + "ModeToggleDefault": "умный режим", + "AccessibilityModeToggle": "переключить режим скачивания", + "DonateLinksDescription": "ссылки на донаты открываются в новой вкладке. это лучший метод пожертвовать деньги, если ты хочешь, чтобы я получил их лично, а не в виде крипто.", + "SettingsAudioFormatBest": "лучший", + "SettingsAudioFormatDescription": "когда выбран \"лучший\" формат, ты получишь аудио максимально возможного качества, так как оно останется в оригинальном формате. если же выбрано что-то другое, то аудио будет немного сжато.", + "Keyphrase": "сохраняй то, что любишь", + "SettingsDisableChangelogOnUpdate": "не показывать изменения после обновлений" } } diff --git a/src/modules/api.js b/src/modules/api.js index e35aa6d2..9e956aad 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -7,7 +7,7 @@ import { errorUnsupported } from "./sub/errors.js"; import loc from "../localization/manager.js"; import match from "./match.js"; -export async function getJSON(originalURL, ip, lang, format, quality) { +export async function getJSON(originalURL, ip, lang, format, quality, audioFormat, isAudioOnly) { try { let url = decodeURI(originalURL); if (!url.includes('http://')) { @@ -28,7 +28,7 @@ export async function getJSON(originalURL, ip, lang, format, quality) { if (patternMatch) break; } if (patternMatch) { - return await match(host, patternMatch, url, ip, lang, format, quality); + return await match(host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly); } return apiJSON(0, { t: errorUnsupported(lang) } ) } return apiJSON(0, { t: errorUnsupported(lang) } ) } else { diff --git a/src/modules/config.js b/src/modules/config.js index b25e473f..a0a4521f 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -15,4 +15,5 @@ supportedLanguages = config.supportedLanguages, quality = config.quality, internetExplorerRedirect = config.internetExplorerRedirect, donations = config.donations, -ffmpegArgs = config.ffmpegArgs +ffmpegArgs = config.ffmpegArgs, +supportedAudio = config.supportedAudio diff --git a/src/modules/emoji.js b/src/modules/emoji.js new file mode 100644 index 00000000..705913a3 --- /dev/null +++ b/src/modules/emoji.js @@ -0,0 +1,24 @@ +const names = { + "🎶": "musical_notes", + "🎬": "clapper_board", + "💰": "money_bag", + "🎉": "party_popper", + "❓": "red_question_mark", + "✨": "sparkles", + "🪅": "pinata", + "🪄": "magic_wand", + "🐲": "dragon_face", + "💸": "money_with_wings", + "⚙️": "gear" +} +let sizing = { + 22: 0.4, + 30: 0.7 +} +export default function(emoji, size, disablePadding) { + if (!size) size = 22; + let padding = size != 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;`: ``; + if (disablePadding) padding = 'margin-right:0!important;'; + if (!names[emoji]) emoji = "❓"; + return `` +} diff --git a/src/modules/match.js b/src/modules/match.js index 16665ea6..9450a16e 100644 --- a/src/modules/match.js +++ b/src/modules/match.js @@ -11,8 +11,9 @@ import vk from "./services/vk.js"; import tiktok from "./services/tiktok.js"; import douyin from "./services/douyin.js"; import tumblr from "./services/tumblr.js"; +import matchActionDecider from "./sub/matchActionDecider.js"; -export default async function (host, patternMatch, url, ip, lang, format, quality) { +export default async function (host, patternMatch, url, ip, lang, format, quality, audioFormat, isAudioOnly) { try { if (!testers[host]) return apiJSON(0, { t: errorUnsupported(lang) }); if (!(testers[host](patternMatch))) throw Error(); @@ -24,99 +25,69 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit id: patternMatch["id"], lang: lang }); - return (!r.error) ? apiJSON(1, { u: r }) : apiJSON(0, { t: r.error }); - + break; case "vk": r = await vk({ userId: patternMatch["userId"], videoId: patternMatch["videoId"], lang: lang, quality: quality }); - return (!r.error) ? apiJSON(2, { type: "bridge", lang: lang, u: r.url, filename: r.filename, - service: host, ip: ip, salt: process.env.streamSalt }) : apiJSON(0, { t: r.error }); - + break; case "bilibili": r = await bilibili({ id: patternMatch["id"].slice(0, 12), lang: lang }); - return (!r.error) ? apiJSON(2, { - type: "render", u: r.urls, lang: lang, - service: host, ip: ip, - filename: r.filename, - salt: process.env.streamSalt, time: r.time - }) : apiJSON(0, { t: r.error }); - + break; case "youtube": let fetchInfo = { id: patternMatch["id"].slice(0,11), lang: lang, quality: quality, - format: "mp4" + format: "webm" }; - if (url.match('music.youtube.com')) { - format = "audio" - } + if (url.match('music.youtube.com') || isAudioOnly == true) format = "audio"; switch (format) { - case "webm": - fetchInfo["format"] = "webm"; + case "mp4": + fetchInfo["format"] = "mp4"; break; case "audio": fetchInfo["format"] = "webm"; fetchInfo["isAudioOnly"] = true; fetchInfo["quality"] = "max"; + isAudioOnly = true; break; } r = await youtube(fetchInfo); - return (!r.error) ? apiJSON(2, { - type: r.type, u: r.urls, lang: lang, service: host, ip: ip, - filename: r.filename, salt: process.env.streamSalt, - isAudioOnly: fetchInfo["isAudioOnly"] ? fetchInfo["isAudioOnly"] : false, - time: r.time, - }) : apiJSON(0, { t: r.error }); - + break; case "reddit": r = await reddit({ sub: patternMatch["sub"], id: patternMatch["id"], title: patternMatch["title"], lang: lang, }); - return (!r.error) ? apiJSON(r.typeId, { - type: r.type, u: r.urls, lang: lang, - service: host, ip: ip, - filename: r.filename, salt: process.env.streamSalt - }) : apiJSON(0, { t: r.error }); - + break; case "tiktok": r = await tiktok({ postId: patternMatch["postId"], id: patternMatch["id"], lang: lang, }); - return (!r.error) ? apiJSON(2, { - type: "bridge", u: r.urls, lang: lang, - service: host, ip: ip, - filename: r.filename, salt: process.env.streamSalt - }) : apiJSON(0, { t: r.error }); - + break; case "douyin": r = await douyin({ postId: patternMatch["postId"], id: patternMatch["id"], lang: lang, }); - return (!r.error) ? apiJSON(2, { - type: "bridge", u: r.urls, lang: lang, - service: host, ip: ip, - filename: r.filename, salt: process.env.streamSalt - }) : apiJSON(0, { t: r.error }); - + break; case "tumblr": r = await tumblr({ id: patternMatch["id"], url: url, user: patternMatch["user"] ? patternMatch["user"] : false, lang: lang }); - return (!r.error) ? apiJSON(1, { u: r.split('?')[0] }) : apiJSON(0, { t: r.error }); + break; default: return apiJSON(0, { t: errorUnsupported(lang) }); } + return matchActionDecider(r, host, ip, audioFormat, isAudioOnly) } catch (e) { return apiJSON(0, { t: genericError(lang, host) }) } diff --git a/src/modules/pageRender.js b/src/modules/pageRender.js deleted file mode 100644 index 25b78dcb..00000000 --- a/src/modules/pageRender.js +++ /dev/null @@ -1,205 +0,0 @@ -import { services, appName, authorInfo, version, quality, repo, donations } from "./config.js"; -import { getCommitInfo } from "./sub/currentCommit.js"; -import loc from "../localization/manager.js"; - -let s = services; -let com = getCommitInfo(); - -let enabledServices = Object.keys(s).filter((p) => { - if (s[p].enabled) { - return true - } -}).sort().map((p) => { - if (s[p].alias) { - return s[p].alias - } else { - return p - } -}).join(', ') - -let donate = `` -for (let i in donations) { - donate += `