implement opensearch

This commit is contained in:
James 2024-02-17 17:52:34 -05:00
parent e6e9f4f099
commit 4887aaf559
No known key found for this signature in database
GPG Key ID: 1B93E9AA216CF0E5
7 changed files with 311 additions and 268 deletions

3
.gitignore vendored
View File

@ -24,3 +24,6 @@ docker-compose.yml
# cookie file # cookie file
cookies.json cookies.json
# opensearch xml build
src/front/opensearch*

View File

@ -610,6 +610,11 @@ window.onload = () => {
detectColorScheme(); detectColorScheme();
popup("migration", 1); popup("migration", 1);
} }
if (pageQuery.has("opensearchquery")) {
eid("url-input-area").value = pageQuery.get("opensearchquery");
button();
eid("download-button").click();
}
window.history.replaceState(null, '', window.location.pathname); window.history.replaceState(null, '', window.location.pathname);
notificationCheck(); notificationCheck();

View File

@ -157,6 +157,9 @@
"SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.", "SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.",
"UpdateTwitterGif": "twitter gifs and pinterest", "UpdateTwitterGif": "twitter gifs and pinterest",
"ErrorTweetProtected": "this tweet is from a private account, so i can't see it. try another one!", "ErrorTweetProtected": "this tweet is from a private account, so i can't see it. try another one!",
"ErrorTweetNSFW": "this tweet contains sensitive content, so i can't see it. try another one!" "ErrorTweetNSFW": "this tweet contains sensitive content, so i can't see it. try another one!",
"ErrorOpenSearch": "<!-- the opensearch xml file failed to build for language {s} :( -->",
"OpenSearchTitle": "Cobalt",
"OpenSearchDescription": "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!"
} }
} }

View File

@ -159,6 +159,9 @@
"SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.", "SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.",
"UpdateTwitterGif": "гифки с твиттера и одноклассники", "UpdateTwitterGif": "гифки с твиттера и одноклассники",
"ErrorTweetProtected": "этот твит из закрытого аккаунта, поэтому я не могу его увидеть. попробуй другой!", "ErrorTweetProtected": "этот твит из закрытого аккаунта, поэтому я не могу его увидеть. попробуй другой!",
"ErrorTweetNSFW": "этот твит содержит деликатный контент, поэтому я не могу его увидеть. попробуй другой!" "ErrorTweetNSFW": "этот твит содержит деликатный контент, поэтому я не могу его увидеть. попробуй другой!",
"ErrorOpenSearch": "<!-- TODO TRANSLATE ME!! the opensearch xml file failed to build for language {s} :( -->",
"OpenSearchTitle": "TODO TRANSLATE ME!! Cobalt",
"OpenSearchDescription": "TODO TRANSLATE ME!! 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!"
} }
} }

View File

@ -4,6 +4,7 @@ import { loadLoc, languageList } from "../localization/manager.js";
import { cleanHTML } from "./sub/utils.js"; import { cleanHTML } from "./sub/utils.js";
import page from "./pageRender/page.js"; import page from "./pageRender/page.js";
import buildOpenSearch from "./pageRender/buildOpenSearch.js";
export async function buildFront(commitHash, branch) { export async function buildFront(commitHash, branch) {
try { try {
@ -31,6 +32,9 @@ export async function buildFront(commitHash, branch) {
} }
fs.writeFileSync(`./build/pc/${i}.html`, cleanHTML(page(params))); fs.writeFileSync(`./build/pc/${i}.html`, cleanHTML(page(params)));
// build opensearch xml
fs.writeFileSync(`./src/front/opensearch-${i}.xml`, cleanHTML(buildOpenSearch(i)));
params["useragent"] = "iphone os"; params["useragent"] = "iphone os";
fs.writeFileSync(`./build/ios/${i}.html`, cleanHTML(page(params))); fs.writeFileSync(`./build/ios/${i}.html`, cleanHTML(page(params)));

View File

@ -0,0 +1,21 @@
import loc from "../../localization/manager.js";
export default function (lang) {
const t = (str, replace) => { return loc(lang, str, replace) };
const strippedURL = process.env.webURL.replace(/^https?:\/\//, '').replace(/\/$/, '');
try {
return `
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>${t('OpenSearchTitle')}</ShortName>
<Description>${t("OpenSearchDescription")}</Description>
<Url type="text/html" method="get" template="${process.env.webURL}?opensearchquery={searchTerms}" />
<Image height="48" width="48" type="image/x-icon">${process.env.webURL}icons/favicon.ico</Image>
<Tags>cobalt download</Tags>
</OpenSearchDescription>
`
} catch (err) {
return `${t('ErrorOpenSearchBuildFail', lang)}`;
}
}

View File

@ -38,6 +38,8 @@ export default function(obj) {
audioFormats[0]["text"] = t('SettingsAudioFormatBest'); audioFormats[0]["text"] = t('SettingsAudioFormatBest');
const strippedURL = process.env.webURL.replace(/^https?:\/\//, '').replace(/\/$/, '');
try { try {
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
@ -56,6 +58,8 @@ export default function(obj) {
<meta name="description" content="${t('AboutSummary')}"> <meta name="description" content="${t('AboutSummary')}">
<meta name="theme-color" content="#000000"> <meta name="theme-color" content="#000000">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<link rel="search" href="/opensearch-${obj.lang}.xml" type="application/opensearchdescription+xml" title="${t('OpenSearchTitle')}">
<meta name="apple-mobile-web-app-capable" content="yes"> <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-status-bar-style" content="black-translucent">
@ -100,61 +104,61 @@ export default function(obj) {
name: "services", name: "services",
title: `${emoji("🔗")} ${t("CollapseServices")}`, title: `${emoji("🔗")} ${t("CollapseServices")}`,
body: `${enabledServices}` body: `${enabledServices}`
+ `<div class="explanation embedded">${t("SupportNotAffiliated")}` + `<div class="explanation embedded">${t("SupportNotAffiliated")}`
+ `${obj.lang === "ru" ? `<br>${t("SupportMetaNoticeRU")}` : ''}` + `${obj.lang === "ru" ? `<br>${t("SupportMetaNoticeRU")}` : ''}`
+ `</div>` + `</div>`
+ `${t("ServicesNote")}` + `${t("ServicesNote")}`
}, { }, {
name: "keyboard", name: "keyboard",
title: `${emoji("⌨")} ${t("CollapseKeyboard")}`, title: `${emoji("⌨")} ${t("CollapseKeyboard")}`,
body: body:
`${t("KeyboardShortcutsIntro")} `${t("KeyboardShortcutsIntro")}
${keyboardShortcuts([{ ${keyboardShortcuts([{
items: [{ items: [{
combo: "Shift+D", combo: "Shift+D",
name: t("PasteFromClipboard") name: t("PasteFromClipboard")
}, {
combo: "Shift+K",
name: t("ModeToggleAuto")
}, {
combo: "Shift+L",
name: t("ModeToggleAudio")
}]
}, { }, {
combo: "Shift+K", items: [{
name: t("ModeToggleAuto") combo: "⌘/Ctrl+V",
name: t("KeyboardShortcutQuickPaste")
}, {
combo: "Esc",
name: t("KeyboardShortcutClear")
}, {
combo: "Esc",
name: t("KeyboardShortcutClosePopup")
}]
}, { }, {
combo: "Shift+L", items: [{
name: t("ModeToggleAudio") combo: "Shift+B",
}] name: t("AboutTab")
}, { }, {
items: [{ combo: "Shift+N",
combo: "⌘/Ctrl+V", name: t("ChangelogTab")
name: t("KeyboardShortcutQuickPaste") }, {
}, { combo: "Shift+M",
combo: "Esc", name: t("TitlePopupSettings")
name: t("KeyboardShortcutClear") }]
}, { }])}`
combo: "Esc",
name: t("KeyboardShortcutClosePopup")
}]
}, {
items: [{
combo: "Shift+B",
name: t("AboutTab")
}, {
combo: "Shift+N",
name: t("ChangelogTab")
}, {
combo: "Shift+M",
name: t("TitlePopupSettings")
}]
}])}`
}, { }, {
name: "support", name: "support",
title: `${emoji("❤️‍🩹")} ${t("CollapseSupport")}`, title: `${emoji("❤️‍🩹")} ${t("CollapseSupport")}`,
body: `${t("SupportSelfTroubleshooting")}` body: `${t("SupportSelfTroubleshooting")}`
+ `${socialLink(emoji("📢"), t("StatusPage"), links.statusPage)}` + `${socialLink(emoji("📢"), t("StatusPage"), links.statusPage)}`
+ `${socialLink(emoji("🔧"), t("TroubleshootingGuide"), links.troubleshootingGuide)}` + `${socialLink(emoji("🔧"), t("TroubleshootingGuide"), links.troubleshootingGuide)}`
+ `<br>` + `<br>`
+ `${t("FollowSupport")}` + `${t("FollowSupport")}`
+ `${socialLinks(obj.lang)}` + `${socialLinks(obj.lang)}`
+ `<br>` + `<br>`
+ `${t("SourceCode")}` + `${t("SourceCode")}`
+ `${socialLink(emoji("🐙"), repo.replace("https://github.com/", ''), repo)}` + `${socialLink(emoji("🐙"), repo.replace("https://github.com/", ''), repo)}`
}, { }, {
name: "privacy", name: "privacy",
title: `${emoji("🔒")} ${t("CollapsePrivacy")}`, title: `${emoji("🔒")} ${t("CollapsePrivacy")}`,
@ -166,14 +170,14 @@ export default function(obj) {
}]) }])
}, },
...(process.env.showSponsors ? ...(process.env.showSponsors ?
[{ [{
text: t("SponsoredBy"), text: t("SponsoredBy"),
classes: ["sponsored-by-text"], classes: ["sponsored-by-text"],
nopadding: true nopadding: true
}, { }, {
text: sponsoredList(), text: sponsoredList(),
raw: true raw: true
}] : [] }] : []
)] )]
}) })
}, { }, {
@ -194,11 +198,11 @@ export default function(obj) {
if (!banner) return ''; if (!banner) return '';
return `<div class="changelog-banner"> return `<div class="changelog-banner">
<img class="changelog-img" ` + <img class="changelog-img" ` +
`src="${banner.url}" ` + `src="${banner.url}" ` +
`alt="${banner.alt.replaceAll('"', '&quot;')}" ` + `alt="${banner.alt.replaceAll('"', '&quot;')}" ` +
`width="${banner.width}" ` + `width="${banner.width}" ` +
`height="${banner.height}" ` + `height="${banner.height}" ` +
`onerror="this.style.opacity=0" loading="lazy"> `onerror="this.style.opacity=0" loading="lazy">
</div>`; </div>`;
})(), })(),
raw: true raw: true
@ -252,11 +256,11 @@ export default function(obj) {
text: ` text: `
<div class="changelog-banner"> <div class="changelog-banner">
<img class="changelog-img" ` + <img class="changelog-img" ` +
`src="updateBanners/catsleep.webp" ` + `src="updateBanners/catsleep.webp" ` +
`alt="${t("DonateImageDescription")}" ` + `alt="${t("DonateImageDescription")}" ` +
`width="480" ` + `width="480" ` +
`height="270" ` + `height="270" ` +
`onerror="this.style.opacity=0" loading="lazy"> `onerror="this.style.opacity=0" loading="lazy">
</div>`, </div>`,
raw: true raw: true
}, { }, {
@ -326,58 +330,58 @@ export default function(obj) {
}] }]
}) })
}) })
+ settingsCategory({ + settingsCategory({
name: "tiktok-watermark", name: "tiktok-watermark",
title: "tiktok", title: "tiktok",
body: checkbox([{ body: checkbox([{
action: "disableTikTokWatermark", action: "disableTikTokWatermark",
name: t("SettingsRemoveWatermark"), name: t("SettingsRemoveWatermark"),
padding: "no-margin" padding: "no-margin"
}]) }])
})
+ settingsCategory({
name: "twitter",
title: "twitter",
body: checkbox([{
action: "twitterGif",
name: t("SettingsTwitterGif"),
padding: "no-margin"
}])
+ explanation(t('SettingsTwitterGifDescription'))
})
+ settingsCategory({
name: "codec",
title: t('SettingsCodecSubtitle'),
body: switcher({
name: "vCodec",
explanation: t('SettingsCodecDescription'),
items: [{
action: "h264",
text: "h264 (mp4)"
}, {
action: "av1",
text: "av1 (mp4)"
}, {
action: "vp9",
text: "vp9 (webm)"
}]
}) })
}) + settingsCategory({
+ settingsCategory({ name: "twitter",
name: "vimeo", title: "twitter",
title: t('SettingsVimeoPrefer'), body: checkbox([{
body: switcher({ action: "twitterGif",
name: "vimeoDash", name: t("SettingsTwitterGif"),
explanation: t('SettingsVimeoPreferDescription'), padding: "no-margin"
items: [{ }])
action: "false", + explanation(t('SettingsTwitterGifDescription'))
text: "progressive" })
}, { + settingsCategory({
action: "true", name: "codec",
text: "dash" title: t('SettingsCodecSubtitle'),
}] body: switcher({
name: "vCodec",
explanation: t('SettingsCodecDescription'),
items: [{
action: "h264",
text: "h264 (mp4)"
}, {
action: "av1",
text: "av1 (mp4)"
}, {
action: "vp9",
text: "vp9 (webm)"
}]
})
})
+ settingsCategory({
name: "vimeo",
title: t('SettingsVimeoPrefer'),
body: switcher({
name: "vimeoDash",
explanation: t('SettingsVimeoPreferDescription'),
items: [{
action: "false",
text: "progressive"
}, {
action: "true",
text: "dash"
}]
})
}) })
})
}, { }, {
name: "audio", name: "audio",
title: `${emoji("🎶")} ${t('SettingsAudioTab')}`, title: `${emoji("🎶")} ${t('SettingsAudioTab')}`,
@ -389,39 +393,39 @@ export default function(obj) {
explanation: t('SettingsAudioFormatDescription'), explanation: t('SettingsAudioFormatDescription'),
items: audioFormats items: audioFormats
}) })
+ sep(0) + sep(0)
+ checkbox([{ + checkbox([{
action: "muteAudio", action: "muteAudio",
name: t("SettingsVideoMute"), name: t("SettingsVideoMute"),
padding: "no-margin" padding: "no-margin"
}]) }])
+ explanation(t('SettingsVideoMuteExplanation')) + explanation(t('SettingsVideoMuteExplanation'))
}) })
+ settingsCategory({ + settingsCategory({
name: "dub", name: "dub",
title: t("SettingsAudioDub"), title: t("SettingsAudioDub"),
body: switcher({ body: switcher({
name: "dubLang", name: "dubLang",
explanation: t('SettingsAudioDubDescription'), explanation: t('SettingsAudioDubDescription'),
items: [{ items: [{
action: "original", action: "original",
text: t('SettingsDubDefault') text: t('SettingsDubDefault')
}, { }, {
action: "auto", action: "auto",
text: t('SettingsDubAuto') text: t('SettingsDubAuto')
}] }]
})
})
+ settingsCategory({
name: "tiktok-audio",
title: "tiktok",
body: checkbox([{
action: "fullTikTokAudio",
name: t("SettingsAudioFullTikTok"),
padding: "no-margin"
}])
+ explanation(t('SettingsAudioFullTikTokDescription'))
}) })
})
+ settingsCategory({
name: "tiktok-audio",
title: "tiktok",
body: checkbox([{
action: "fullTikTokAudio",
name: t("SettingsAudioFullTikTok"),
padding: "no-margin"
}])
+ explanation(t('SettingsAudioFullTikTokDescription'))
})
}, { }, {
name: "other", name: "other",
title: `${emoji("🪅")} ${t('SettingsOtherTab')}`, title: `${emoji("🪅")} ${t('SettingsOtherTab')}`,
@ -442,26 +446,26 @@ export default function(obj) {
}] }]
}) })
}) })
+ settingsCategory({ + settingsCategory({
name: "filename", name: "filename",
title: t('FilenameTitle'), title: t('FilenameTitle'),
body: switcher({ body: switcher({
name: "filenamePattern", name: "filenamePattern",
items: [{ items: [{
action: "classic", action: "classic",
text: t('FilenamePatternClassic') text: t('FilenamePatternClassic')
}, { }, {
action: "basic", action: "basic",
text: t('FilenamePatternBasic') text: t('FilenamePatternBasic')
}, { }, {
action: "pretty", action: "pretty",
text: t('FilenamePatternPretty') text: t('FilenamePatternPretty')
}, { }, {
action: "nerdy", action: "nerdy",
text: t('FilenamePatternNerdy') text: t('FilenamePatternNerdy')
}] }]
}) })
+ `<div id="filename-preview"> + `<div id="filename-preview">
<div id="video-filename" class="filename-item line"> <div id="video-filename" class="filename-item line">
${emoji('🎞️', 32, 1, 1)} ${emoji('🎞️', 32, 1, 1)}
<div class="filename-container"> <div class="filename-container">
@ -477,40 +481,40 @@ export default function(obj) {
</div> </div>
</div> </div>
</div>` </div>`
+ explanation(t('FilenameDescription')) + explanation(t('FilenameDescription'))
}) })
+ settingsCategory({ + settingsCategory({
name: "accessibility", name: "accessibility",
title: t('Accessibility'), title: t('Accessibility'),
body: checkbox([{ body: checkbox([{
action: "alwaysVisibleButton", action: "alwaysVisibleButton",
name: t("SettingsKeepDownloadButton"), name: t("SettingsKeepDownloadButton"),
aria: t("AccessibilityKeepDownloadButton") aria: t("AccessibilityKeepDownloadButton")
}, { }, {
action: "reduceTransparency", action: "reduceTransparency",
name: t("SettingsReduceTransparency") name: t("SettingsReduceTransparency")
}, { }, {
action: "disableAnimations", action: "disableAnimations",
name: t("SettingsDisableAnimations"), name: t("SettingsDisableAnimations"),
padding: "no-margin" padding: "no-margin"
}]) }])
}) })
+ settingsCategory({ + settingsCategory({
name: "miscellaneous", name: "miscellaneous",
title: t('Miscellaneous'), title: t('Miscellaneous'),
body: checkbox([{ body: checkbox([{
action: "downloadPopup", action: "downloadPopup",
name: t("SettingsEnableDownloadPopup"), name: t("SettingsEnableDownloadPopup"),
aria: t("AccessibilityEnableDownloadPopup") aria: t("AccessibilityEnableDownloadPopup")
}, { }, {
action: "disableMetadata", action: "disableMetadata",
name: t("SettingsDisableMetadata") name: t("SettingsDisableMetadata")
}, { }, {
action: "disableChangelog", action: "disableChangelog",
name: t("SettingsDisableNotifications"), name: t("SettingsDisableNotifications"),
padding: "no-margin" padding: "no-margin"
}]) }])
}) })
}] }]
})} })}
${popupWithBottomButtons({ ${popupWithBottomButtons({
@ -525,62 +529,62 @@ export default function(obj) {
})} })}
<div id="popup-download-container" class="popup-from-bottom"> <div id="popup-download-container" class="popup-from-bottom">
${popup({ ${popup({
name: "download",
standalone: true,
buttonOnly: true,
classes: ["small"],
header: {
closeAria: t('AccessibilityGoBack'),
emoji: emoji("🐱", 78, 1, 1),
title: t('TitlePopupDownload')
},
body: switcher({
name: "download", name: "download",
standalone: true, explanation: `${!isIOS ? t('DownloadPopupDescription') : t('DownloadPopupDescriptionIOS')}`,
buttonOnly: true, items: `<a id="pd-download" class="switch full" target="_blank" href="/"><span>${t('Download')}</span></a>
classes: ["small"],
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-share" class="switch full">${t('ShareURL')}</div>
<div id="pd-copy" class="switch full">${t('CopyURL')}</div>` <div id="pd-copy" class="switch full">${t('CopyURL')}</div>`
}), }),
buttonText: t('PopupCloseDone') buttonText: t('PopupCloseDone')
})} })}
</div> </div>
<div id="popup-error-container" class="popup-from-bottom"> <div id="popup-error-container" class="popup-from-bottom">
${popup({ ${popup({
name: "error", name: "error",
standalone: true, standalone: true,
buttonOnly: true, buttonOnly: true,
classes: ["small"], classes: ["small"],
header: { header: {
title: t('TitlePopupError'), title: t('TitlePopupError'),
emoji: emoji("😿", 78, 1, 1), emoji: emoji("😿", 78, 1, 1),
}, },
body: `<div id="desc-error" class="desc-padding subtext desc-error"></div>`, body: `<div id="desc-error" class="desc-padding subtext desc-error"></div>`,
buttonText: t('ErrorPopupCloseButton') buttonText: t('ErrorPopupCloseButton')
})} })}
</div> </div>
<div id="popup-migration-container" class="popup-from-bottom"> <div id="popup-migration-container" class="popup-from-bottom">
${popup({ ${popup({
name: "migration", name: "migration",
standalone: true, standalone: true,
buttonOnly: true, buttonOnly: true,
classes: ["small"], classes: ["small"],
header: { header: {
title: t('NewDomainWelcomeTitle'), title: t('NewDomainWelcomeTitle'),
emoji: emoji("😸", 78, 1, 1), emoji: emoji("😸", 78, 1, 1),
}, },
body: `<div id="desc-migration" class="desc-padding subtext desc-error">${t('NewDomainWelcome')}</div>`, body: `<div id="desc-migration" class="desc-padding subtext desc-error">${t('NewDomainWelcome')}</div>`,
buttonText: t('ErrorPopupCloseButton') buttonText: t('ErrorPopupCloseButton')
})} })}
<div id="popup-backdrop-message" onclick="popup('message', 0)"></div> <div id="popup-backdrop-message" onclick="popup('message', 0)"></div>
</div> </div>
<div id="popup-backdrop" onclick="hideAllPopups()"></div> <div id="popup-backdrop" onclick="hideAllPopups()"></div>
<div id="home" style="visibility:hidden"> <div id="home" style="visibility:hidden">
${urgentNotice({ ${urgentNotice({
emoji: "🎬", emoji: "🎬",
text: t("UpdateTwitterGif"), text: t("UpdateTwitterGif"),
visible: true, visible: true,
action: "popup('about', 1, 'changelog')" action: "popup('about', 1, 'changelog')"
})} })}
<div id="cobalt-main-box" class="center"> <div id="cobalt-main-box" class="center">
<div id="logo">${t("AppTitleCobalt")}${betaTag()}</div> <div id="logo">${t("AppTitleCobalt")}${betaTag()}</div>
<div id="download-area"> <div id="download-area">
@ -593,37 +597,37 @@ export default function(obj) {
<div id="bottom"> <div id="bottom">
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button> <button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
${switcher({ ${switcher({
name: "audioMode", name: "audioMode",
noParent: true, noParent: true,
items: [{ items: [{
action: "false", action: "false",
text: `${emoji("✨")} ${t("ModeToggleAuto")}` text: `${emoji("✨")} ${t("ModeToggleAuto")}`
}, { }, {
action: "true", action: "true",
text: `${emoji("🎶")} ${t("ModeToggleAudio")}` text: `${emoji("🎶")} ${t("ModeToggleAudio")}`
}] }]
})} })}
</div> </div>
</div> </div>
</div> </div>
<footer id="footer"> <footer id="footer">
${footerButtons([{ ${footerButtons([{
name: "about", name: "about",
type: "popup", type: "popup",
text: `${emoji("🐲" , 22)} ${t('AboutTab')}`, text: `${emoji("🐲" , 22)} ${t('AboutTab')}`,
aria: t('AccessibilityOpenAbout') aria: t('AccessibilityOpenAbout')
}, { }, {
name: "about", name: "about",
type: "popup", type: "popup",
context: "donate", context: "donate",
text: `${emoji("💖", 22)} ${t('Donate')}`, text: `${emoji("💖", 22)} ${t('Donate')}`,
aria: t('AccessibilityOpenDonate') aria: t('AccessibilityOpenDonate')
}, { }, {
name: "settings", name: "settings",
type: "popup", type: "popup",
text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`, text: `${emoji("⚙️", 22)} ${t('TitlePopupSettings')}`,
aria: t('AccessibilityOpenSettings') aria: t('AccessibilityOpenSettings')
}])} }])}
</footer> </footer>
</div> </div>
<script> <script>