diff --git a/package.json b/package.json index 554a24e..f077255 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "4.7.4", + "version": "4.8", "author": "wukko", "exports": "./src/cobalt.js", "type": "module", diff --git a/src/config.json b/src/config.json index 2d800fe..4ce8c81 100644 --- a/src/config.json +++ b/src/config.json @@ -6,7 +6,17 @@ "authorInfo": { "name": "wukko", "link": "https://wukko.me/", - "contact": "https://wukko.me/contacts" + "contact": "https://wukko.me/contacts", + "support": { + "twitter": { + "url": "https://twitter.com/justusecobalt", + "handle": "@justusecobalt" + }, + "mastodon": { + "url": "https://wetdry.world/@cobalt", + "handle": "@cobalt@wetdry.world" + } + } }, "internetExplorerRedirect": { "newNT": ["6.1", "6.2", "6.3", "10.0"], diff --git a/src/front/cobalt.css b/src/front/cobalt.css index fabb6ca..eb10b61 100644 --- a/src/front/cobalt.css +++ b/src/front/cobalt.css @@ -5,6 +5,8 @@ --border-10: 0.1rem solid var(--accent); --font-mono: 'Noto Sans Mono', 'Consolas', 'SF Mono', monospace; --red: rgb(255, 0, 61); + --padding-1: 0.75rem; + --line-height: 1.65rem; } @media (prefers-color-scheme: dark) { :root { @@ -75,10 +77,11 @@ a { [type="checkbox"] { -webkit-appearance: none; appearance: none; - margin-right: 1rem; + margin-right: var(--padding-1); z-index: 0; border: 0; height: 15px; + width: 15px; } [type="checkbox"]::before { content: ""; @@ -96,6 +99,7 @@ a { } .checkbox span { margin-top: 0.21rem; + margin-left: 0.4rem; } button { background: none; @@ -112,17 +116,11 @@ input[type="text"], button:hover, .switch:hover, .checkbox:hover, -.text-to-copy:hover { +.text-to-copy:hover, +.collapse-header:hover { background: var(--accent-hover); cursor: pointer; } -.switch.text-backdrop:hover, -.switch.text-backdrop:active, -.text-to-copy.text-backdrop:hover, -.text-to-copy.text-backdrop:active { - background: var(--accent); - color: var(--background); -} button:active, .switch:active, .checkbox:active, @@ -131,6 +129,17 @@ button:active, cursor: pointer; transform: scale(0.95) } +.collapse-header:active { + background: var(--accent-press); + cursor: pointer; +} +.switch.text-backdrop:hover, +.switch.text-backdrop:active, +.text-to-copy.text-backdrop:hover, +.text-to-copy.text-backdrop:active { + background: var(--accent); + color: var(--background); +} .picker-image:active { cursor: pointer; transform: scale(0.95) @@ -242,7 +251,7 @@ input[type="checkbox"] { #cobalt-main-box #bottom, #footer-buttons, #footer-buttons, .footer-pair { - gap: 0.8rem; + gap: 0.6rem; } #footer-buttons, .footer-pair { display: flex; @@ -265,7 +274,17 @@ input[type="checkbox"] { .text-backdrop { background: var(--accent); color: var(--background); - padding: 0 0.1rem; +} +.italic { + font-style: italic; +} +.social-link { + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 0.3rem; + margin-top: 0.5rem; + user-select: none; } ::-moz-selection { background-color: var(--accent); @@ -312,7 +331,7 @@ input[type="checkbox"] { width: 100%; background-color: var(--accent-button-bg); max-height: 300px; - margin-bottom: 2rem; + margin-bottom: 1.65rem; float: left; } .changelog-img { @@ -332,7 +351,7 @@ input[type="checkbox"] { } #popup-subtitle { font-size: 1.1rem; - padding-bottom: 1rem; + padding-bottom: var(--padding-1); } #popup-desc, #desc-error, @@ -340,7 +359,7 @@ input[type="checkbox"] { width: 100%; text-align: left; float: left; - line-height: 1.7rem; + line-height: var(--line-height); user-select: text; } #popup-title { @@ -359,7 +378,7 @@ input[type="checkbox"] { } .popup-footer-content { font-size: 0.8rem; - line-height: 1.7rem; + line-height: var(--line-height); color: var(--accent-unhover-2); border-top: 0.05rem solid var(--accent-unhover-2); padding-top: 0.4rem; @@ -386,14 +405,8 @@ input[type="checkbox"] { #popup-content.with-footer { margin-bottom: 3rem; } -#popup-close { - cursor: pointer; - float: right; - right: 0; - position: absolute; -} .settings-category { - padding-bottom: 1.2rem; + padding-bottom: 1rem; } .separator { float: left; @@ -408,13 +421,17 @@ input[type="checkbox"] { } .category-title { text-align: left; - line-height: 1.7rem; + line-height: var(--line-height); } .bottom-margin { - margin-bottom: 1rem!important; + margin-bottom: var(--padding-1)!important; } .top-margin { - margin-top: 1rem!important; + margin-top: var(--padding-1)!important; +} +.top-margin-only { + margin-top: var(--padding-1)!important; + margin-bottom: 0!important; } .no-margin { margin: 0!important; @@ -424,10 +441,10 @@ input[type="checkbox"] { align-items: center; flex-direction: row; flex-wrap: nowrap; - align-content: center; padding: 0.55rem 1rem 0.8rem 0.7rem; width: auto; - margin: 0 0.5rem 0.5rem 0; + margin-right: var(--padding-1); + margin-bottom: var(--padding-1); background: var(--accent-button-bg); } .checkbox-label { @@ -439,7 +456,7 @@ input[type="checkbox"] { .subtitle { width: 100%; text-align: left; - line-height: 1.7rem; + line-height: var(--line-height); padding-bottom: 0.4rem; color: var(--accent); } @@ -469,7 +486,7 @@ input[type="checkbox"] { cursor: pointer; } .switch.space-right { - margin-right: 1rem + margin-right: var(--padding-1); } .switch[data-enabled="true"] { color: var(--background); @@ -494,17 +511,23 @@ input[type="checkbox"] { .text-to-copy { user-select: text; border: var(--border-15); - padding: 1rem; + padding: var(--padding-1); overflow: auto; } -#close-bottom { +#close-button { max-width: 2.8rem; - margin-left: 1rem; + margin-left: var(--padding-1); background: var(--background); border: var(--border-15); color: var(--accent); padding: 0.3rem 0.75rem 0.5rem; } +#close-button.up { + float: right; + position: absolute; + right: 0; + height: 2.8rem; +} .popup-tab-content { display: none; } @@ -519,18 +542,7 @@ input[type="checkbox"] { } .emoji { margin-right: 0.4rem; -} -.tooltip { - position: absolute; - margin-top: -6rem; - margin-left: -0.5rem; - line-height: 1.2; - text-align: left; - pointer-events: none; - color: var(--accent-unhover-2)!important; -} -.button:active .tooltip { - display: none; + user-select: none; } .picker-image { object-fit: cover; @@ -540,13 +552,13 @@ input[type="checkbox"] { .picker-image-container { width: 8rem; height: 8rem; - margin-bottom: 1rem; + margin-bottom: var(--padding-1); background-color: var(--accent-button-bg); } .picker-various-container { height: 20rem; width: 25rem; - margin-bottom: 1rem; + margin-bottom: var(--padding-1); background-color: var(--accent-button-bg); position: relative; } @@ -579,7 +591,41 @@ input[type="checkbox"] { } #popup-picker .explanation { margin-top: 0!important; - margin-bottom: 1rem; + margin-bottom: var(--padding-1); +} +.collapse-list { + background: var(--accent-press); + user-select: none; +} +.collapse-header { + padding: var(--padding-1); + font-size: 1rem; + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + background: var(--accent-button-bg); +} +.collapse-indicator { + transform: rotate(180deg); +} +.expanded .collapse-indicator { + transform: none; +} +.collapse-title { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.8rem; +} +.collapse-body { + display: none; + padding: var(--padding-1); + user-select: text; +} +.expanded .collapse-body { + display: block } /* adapt the page according to screen size */ @media screen and (min-width: 2300px) { @@ -655,9 +701,9 @@ input[type="checkbox"] { } } /* mobile page */ -@media screen and (max-width: 949px) { +@media screen and (max-width: 720px) { #cobalt-main-box, #footer { - width: 85%; + width: 90%; } } @media screen and (max-width: 475px) { @@ -699,6 +745,39 @@ input[type="checkbox"] { margin-right: 0; } } +@media screen and (max-width: 720px) { + #cobalt-main-box #bottom { + flex-direction: column; + } + #cobalt-main-box #bottom button { + width: 100%!important; + } + #footer { + bottom: 4%; + transform: translate(-50%, 0%); + } + #footer-buttons { + flex-direction: column; + align-items: stretch; + } + .footer-pair .footer-button { + width: 100%!important; + } + #logo-area { + padding-right: 0; + padding-top: 0; + position: fixed; + line-height: 0; + margin-top: -2rem; + width: 100%; + text-align: center; + } + #cobalt-main-box { + display: flex; + border: none; + padding: 0; + } +} @media screen and (max-width: 949px) { #picker-holder::-webkit-scrollbar { display: none; @@ -715,23 +794,6 @@ input[type="checkbox"] { height: 20rem; max-width: 100%; } - #cobalt-main-box #bottom { - flex-direction: column; - } - #cobalt-main-box #bottom button { - width: 100%!important; - } - #footer { - bottom: 1.7rem; - transform: translate(-50%, 0%); - } - #footer-buttons { - flex-direction: column; - align-items: stretch; - } - .footer-pair .footer-button { - width: 100%!important; - } #popup-header { padding-top: 1.2rem; } @@ -744,24 +806,10 @@ input[type="checkbox"] { line-height: 7rem; } #close-error { - bottom: 5%; + bottom: 3rem; position: absolute; width: var(--without-padding); } - #logo-area { - padding-right: 0; - padding-top: 0; - position: fixed; - line-height: 0; - margin-top: -2rem; - width: 100%; - text-align: center; - } - #cobalt-main-box { - display: flex; - border: none; - padding: 0; - } .popup, .popup.scrollable, .popup.small { border: none; width: 90%; diff --git a/src/front/cobalt.js b/src/front/cobalt.js index 7086ec8..0628d35 100644 --- a/src/front/cobalt.js +++ b/src/front/cobalt.js @@ -1,7 +1,7 @@ let ua = navigator.userAgent.toLowerCase(); let isIOS = ua.match("iphone os"); let isMobile = ua.match("android") || ua.match("iphone os"); -let version = 21; +let version = 22; let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); let notification = `
` @@ -85,8 +85,8 @@ function clearInput() { function copy(id, data) { let e = document.getElementById(id); e.classList.add("text-backdrop"); - data ? navigator.clipboard.writeText(data) : navigator.clipboard.writeText(e.innerText); setTimeout(() => { e.classList.remove("text-backdrop") }, 600); + data ? navigator.clipboard.writeText(data) : navigator.clipboard.writeText(e.innerText); } function detectColorScheme() { let theme = "auto"; @@ -112,6 +112,11 @@ function changeTab(evnt, tabId, tabClass) { if (tabId === "tab-about-changelog" && sGet("changelogStatus") !== `${version}`) notificationCheck("changelog"); if (tabId === "tab-about-about" && !sGet("seenAbout")) notificationCheck("about"); } +function expandCollapsible(evnt) { + let classlist = evnt.currentTarget.parentNode.classList; + let c = "expanded"; + !classlist.contains(c) ? classlist.add(c) : classlist.remove(c); +} function notificationCheck(type) { let changed = true; switch (type) { @@ -123,7 +128,6 @@ function notificationCheck(type) { break; default: changed = false; - break; } if (changed && sGet("changelogStatus") === `${version}` || type === "disable") { setTimeout(() => { diff --git a/src/front/emoji/bird.svg b/src/front/emoji/bird.svg new file mode 100644 index 0000000..55cf020 --- /dev/null +++ b/src/front/emoji/bird.svg @@ -0,0 +1,6 @@ + diff --git a/src/front/emoji/crystal_ball.svg b/src/front/emoji/crystal_ball.svg new file mode 100644 index 0000000..d2a7f67 --- /dev/null +++ b/src/front/emoji/crystal_ball.svg @@ -0,0 +1,7 @@ + diff --git a/src/front/emoji/elephant.svg b/src/front/emoji/elephant.svg new file mode 100644 index 0000000..3f96a89 --- /dev/null +++ b/src/front/emoji/elephant.svg @@ -0,0 +1,8 @@ + diff --git a/src/front/emoji/octopus.svg b/src/front/emoji/octopus.svg new file mode 100644 index 0000000..b8c6e90 --- /dev/null +++ b/src/front/emoji/octopus.svg @@ -0,0 +1,12 @@ + diff --git a/src/front/emoji/red_question_mark.svg b/src/front/emoji/question_mark.svg similarity index 94% rename from src/front/emoji/red_question_mark.svg rename to src/front/emoji/question_mark.svg index 0227702..3c7e887 100644 --- a/src/front/emoji/red_question_mark.svg +++ b/src/front/emoji/question_mark.svg @@ -1,4 +1,4 @@ diff --git a/src/front/updateBanners/catmakeup.webp b/src/front/updateBanners/catmakeup.webp new file mode 100644 index 0000000..baaf8e0 Binary files /dev/null and b/src/front/updateBanners/catmakeup.webp differ diff --git a/src/localization/languages/en.json b/src/localization/languages/en.json index 57a8a2d..3e70c30 100644 --- a/src/localization/languages/en.json +++ b/src/localization/languages/en.json @@ -1,12 +1,11 @@ { "name": "english", "substrings": { - "ContactLink": "file an issue on github" + "ContactLink": "file an issue on github" }, "strings": { "LinkInput": "paste the link here", "AboutSummary": "{appName} is your go-to place for social media downloads. zero ads, trackers, or any other creepy bullshit. simply paste a share link and you're ready to rock!", - "AboutSupportedServices": "currently supported services:", "EmbedBriefDescription": "save content from social media without annoyances", "MadeWithLove": "made with <3 by wukko", "AccessibilityInputArea": "link input area", @@ -53,7 +52,6 @@ "AccessibilityEnableDownloadPopup": "ask what to do with downloads", "SettingsFormatDescription": "select webm if you want max quality available. webm videos are usually higher bitrate, but ios devices can't play them natively.", "SettingsQualityDescription": "if selected quality isn't available, closest one gets picked instead.\nif you want to post a youtube video on social media, select a combination of mp4 and 720p.", - "LinkGitHubIssues": ">> report issues and check out the source code on github", "LinkGitHubChanges": ">> see previous commits and contribute on github", "NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.", "DownloadPopupDescriptionIOS": "press and hold the download button, hide the video preview, and then select \"download linked file\" to save.", @@ -88,7 +86,6 @@ "ErrorNoUrlReturned": "server didn't return a download link. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.", "ErrorUnknownStatus": "i received a response i can't process. most likely something with status is wrong. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.", "PasteFromClipboard": "paste from clipboard", - "FollowTwitter": "follow {appName}'s twitter account for polls, updates, and more: @justusecobalt", "ChangelogOlder": "previous versions", "ChangelogPressToExpand": "press to expand", "Miscellaneous": "miscellaneous", @@ -104,12 +101,20 @@ "ChangelogPressToHide": "press to collapse", "Donate": "donate", "DonateSub": "help me keep it up", - "DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's completely free to use. but hey! turns out keeping up a web service used by hundreds of thousands of people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated.", + "DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's completely free to use. but turns out keeping up a web service used by thousands of people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated.", "DonateVia": "donate via", - "DonateHireMe": "or, as an alternative, you can hire me.", + "DonateHireMe": "or, as an alternative, you can hire me.", "SettingsVideoMute": "mute audio", "SettingsVideoMuteExplanation": "disables audio in downloaded video when possible. ignored when audio mode is on or service only supports audio.", "SettingsVideoGeneral": "general", - "ErrorSoundCloudNoClientId": "couldn't find client_id that is required to fetch audio data from soundcloud. try again, and if issue persists, {ContactLink}." + "ErrorSoundCloudNoClientId": "couldn't find client_id that is required to fetch audio data from soundcloud. try again, and if issue persists, {ContactLink}.", + "CollapseServices": "supported services", + "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": "follow {appName} on mastodon or twitter 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.", + "SourceCode": "report issues, explore source code, star or fork the repo:", + "PrivacyPolicy": "{appName}'s privacy policy is simple: no data about you is 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, salted sha256 hash of your ip address and information about requested stream are temporarily stored in server's RAM for 2 minutes. after 2 minutes all previously stored information is permanently removed. hash of your ip address is used for limiting stream access only to you.\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 in the first place.\n\nyou can check {appName}'s github repo for yourself and see that indeed nothing is stored permanently." } } diff --git a/src/localization/languages/ru.json b/src/localization/languages/ru.json index b6816d1..e09b016 100644 --- a/src/localization/languages/ru.json +++ b/src/localization/languages/ru.json @@ -1,12 +1,11 @@ { "name": "русский", "substrings": { - "ContactLink": "напиши об этом на github" + "ContactLink": "напиши об этом на github" }, "strings": { "LinkInput": "вставь ссылку сюда", "AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку и получаешь файл. ничего лишнего.", - "AboutSupportedServices": "что поддерживается:", "EmbedBriefDescription": "сохраняй что хочешь, без мороки и вторжения в личное пространство", "MadeWithLove": "сделано с <3 wukko", "AccessibilityInputArea": "зона вставки ссылки", @@ -53,7 +52,6 @@ "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. у webm видео битрейт обычно выше, но устройства на ios не могут проигрывать их без сторонних приложений.", "SettingsQualityDescription": "если выбранное качество недоступно, то выбирается ближайшее к нему.\nесли ты хочешь опубликовать видео с youtube где-то в соц. сетях, то выбирай комбинацию из mp4 и 720p.", - "LinkGitHubIssues": ">> сообщай о проблемах и смотри исходный код на github", "LinkGitHubChanges": ">> смотри предыдущие изменения на github", "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.", "DownloadPopupDescriptionIOS": "зажми кнопку \"скачать\", затем скрой превью видео и выбери \"загрузить файл по ссылке\" в появившемся окне.", @@ -88,7 +86,6 @@ "ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.", "ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.", "PasteFromClipboard": "вставить из буфера обмена", - "FollowTwitter": "а ещё, в твиттере {appName} есть опросы, новости, и многое другое: @justusecobalt", "ChangelogOlder": "предыдущие версии (на английском)", "ChangelogPressToExpand": "нажми, чтобы раскрыть", "Miscellaneous": "разное", @@ -104,12 +101,20 @@ "ChangelogPressToHide": "нажми, чтобы скрыть", "Donate": "донаты", "DonateSub": "ты можешь помочь!", - "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что хостинг сервиса, которым пользуются сотни тысяч людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!", + "DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает совершенно бесплатно. но оказывается, что хостинг сервиса, которым пользуются тысячи людей, обходится довольно дорого.\n\nесли ты хочешь, чтобы твой любимый загрузчик оставался онлайн, а разработчик не помер с голоду вместе с двумя котами, то подумай над тем, чтобы задонатить. каждый рубль поможет мне, моим котам, и {appName}!", "DonateVia": "открыть", - "DonateHireMe": "или же ты можешь пригласить меня на работу.", + "DonateHireMe": "или же ты можешь пригласить меня на работу.", "SettingsVideoMute": "отключить аудио", "SettingsVideoMuteExplanation": "убирает аудио при загрузке видео, когда это возможно. игнорируется если включен режим аудио или сервис поддерживает только аудио загрузки.", "SettingsVideoGeneral": "основные", - "ErrorSoundCloudNoClientId": "мне не удалось достать client_id, который необходим для получения аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}." + "ErrorSoundCloudNoClientId": "мне не удалось достать client_id, который необходим для получения аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.", + "CollapseServices": "что поддерживается?", + "CollapseSupport": "поддержка и исходный код", + "CollapsePrivacy": "политика конфиденциальности", + "ServicesNote": "этот список далеко не финальный и постоянно пополняется. заглядывай сюда почаще, тогда точно будешь знать, что поддерживается!", + "FollowSupport": "подписывайся на аккаунты {appName} на mastodon или twitter для новостей, поддержки, участия в опросах, и многого другого:", + "SupportNote": "помни, что ответ на твой вопрос может занять время, так как только один человек занимается и разработкой и поддержкой.", + "SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:", + "PrivacyPolicy": "политика конфиденциальности {appName} довольно проста: ничего не хранится об истории твоих действий или загрузок. совсем. даже ошибки.\nто, что ты скачиваешь - не моё дело, а только твоё.\n\nв случаях, когда твоей загрузке требуется лайв-рендер, временно хранится неотслеживаемая информация. это необходимо для работы данной функции.\n\nв этом случае, sha256 хэш (с солью) твоего ip адреса и данные о запрошенном стриме хранятся в оперативной памяти сервера в течение 2-х минут. по истечении этого времени всё стирается. хэш твоего ip адреса используется для предоставления доступа к стриму только тебе. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как код {appName} специально не позволяет читать такие данные снаружи.\n\nты можешь посмотреть исходный код {appName} и убедиться, что всё так, как описано." } } diff --git a/src/localization/manager.js b/src/localization/manager.js index 2db0774..44d80c8 100644 --- a/src/localization/manager.js +++ b/src/localization/manager.js @@ -15,7 +15,7 @@ export function loadLoc() { } loadLoc(); export function replaceBase(s) { - return s.replace(/\n/g, '