web: base custom instance functionality
also: - renamed processing tab in settings to "instances" - improved override description - prefer custom over override (and grey out the option) - dedicated lib for all api safety warnings - left aligned small popup with smaller icon - ability to grey out settings category & toggle
This commit is contained in:
parent
70c1a85766
commit
33d6b5bd81
16 changed files with 278 additions and 39 deletions
|
@ -11,5 +11,6 @@
|
|||
"import": "import",
|
||||
"continue": "continue",
|
||||
"star": "star",
|
||||
"follow": "follow"
|
||||
"follow": "follow",
|
||||
"save": "save"
|
||||
}
|
||||
|
|
|
@ -15,5 +15,7 @@
|
|||
"import.body": "importing unknown or corrupted files may unexpectedly alter or break cobalt functionality. only import files that you've personally exported and haven't modified. if you were asked to import this file by someone - don't do it.\n\nwe are not responsible for any harm caused by importing unknown setting files.",
|
||||
|
||||
"api.override.title": "processing instance override",
|
||||
"api.override.body": "{{ value }} is now the processing instance. if you don't trust it, press \"cancel\" and it'll be ignored.\n\nyou can change your choice later in processing settings."
|
||||
"api.override.body": "{{ value }} is now the processing instance. if you don't trust it, press \"cancel\" and it'll be ignored.\n\nyou can change your choice later in processing settings.",
|
||||
|
||||
"safety.custom_instance.body": "custom instances can potentially pose privacy & safety risks.\n\nbad instances can:\n1. redirect you away from cobalt and try to scam you.\n2. log all information about your requests, store it forever, and use it to track you.\n3. serve you malicious files (such as malware).\n4. force you to watch ads, or make you pay for downloading.\n\nafter this point, we can't protect you. please be mindful of what instances to use and always trust your gut. if anything feels off, come back to this page, reset the custom instance, and report it to us on github."
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"page.download": "downloading",
|
||||
"page.advanced": "advanced",
|
||||
"page.debug": "debug information",
|
||||
"page.processing": "processing",
|
||||
"page.instances": "instances",
|
||||
|
||||
"section.general": "general",
|
||||
"section.save": "save",
|
||||
|
@ -112,5 +112,10 @@
|
|||
|
||||
"processing.override": "default instance override",
|
||||
"processing.override.title": "use instance-provided processing server",
|
||||
"processing.override.description": "cobalt will use the processing server from DEFAULT_API when this is enabled. this is a temporary description."
|
||||
"processing.override.description": "if web instance provides its own default processing server, you can choose to use it over the main processing server. make sure it's a server by someone you trust.",
|
||||
|
||||
"processing.community": "community instances",
|
||||
|
||||
"processing.enable_custom.title": "use a custom processing server",
|
||||
"processing.enable_custom.description": "cobalt will use a custom processing server if you choose to. even though cobalt has some security measures in place, we are not responsible for any damage done via a community instance, as we have no control over them.\n\nplease be mindful of what instances you use and make sure they're hosted by people you trust."
|
||||
}
|
||||
|
|
|
@ -10,28 +10,37 @@
|
|||
|
||||
import Toggle from "$components/misc/Toggle.svelte";
|
||||
|
||||
export let settingContext: Context;
|
||||
export let settingId: Id;
|
||||
export let settingContext: Context;
|
||||
|
||||
export let title: string;
|
||||
export let description: string = "";
|
||||
|
||||
export let disabled = false;
|
||||
export let disabledOpacity = false;
|
||||
|
||||
$: setting = $settings[settingContext][settingId];
|
||||
$: isEnabled = !!setting;
|
||||
</script>
|
||||
|
||||
<div id="setting-toggle-{settingContext}-{String(settingId)}" class="toggle-parent">
|
||||
<div
|
||||
id="setting-toggle-{settingContext}-{String(settingId)}"
|
||||
class="toggle-parent"
|
||||
class:disabled
|
||||
class:faded={disabledOpacity}
|
||||
aria-hidden={disabled}
|
||||
>
|
||||
<button
|
||||
class="toggle-container"
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
disabled={disabled}
|
||||
on:click={() =>
|
||||
updateSetting({
|
||||
[settingContext]: {
|
||||
[settingId]: !isEnabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
})}
|
||||
>
|
||||
<h4 class="toggle-title">{title}</h4>
|
||||
<Toggle enabled={isEnabled} />
|
||||
|
@ -51,6 +60,15 @@
|
|||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.toggle-parent.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toggle-parent.faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
|
|
|
@ -18,12 +18,17 @@
|
|||
export let bodySubText = "";
|
||||
export let buttons: Optional<DialogButton[]> = undefined;
|
||||
export let dismissable = true;
|
||||
export let leftAligned = false;
|
||||
|
||||
let close: () => void;
|
||||
</script>
|
||||
|
||||
<DialogContainer {id} {dismissable} bind:close>
|
||||
<div class="dialog-body small-dialog" class:meowbalt-visible={meowbalt}>
|
||||
<div
|
||||
class="dialog-body small-dialog"
|
||||
class:meowbalt-visible={meowbalt}
|
||||
class:align-left={leftAligned}
|
||||
>
|
||||
{#if meowbalt}
|
||||
<div class="meowbalt-container">
|
||||
<Meowbalt emotion={meowbalt} />
|
||||
|
@ -46,7 +51,7 @@
|
|||
<div class="body-text" tabindex="-1">{bodyText}</div>
|
||||
{/if}
|
||||
{#if bodySubText}
|
||||
<div class="subtext">{bodySubText}</div>
|
||||
<div class="subtext popup-subtext">{bodySubText}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if buttons}
|
||||
|
@ -72,7 +77,7 @@
|
|||
text-align: center;
|
||||
max-width: 340px;
|
||||
width: calc(100% - var(--padding) - var(--dialog-padding) * 2);
|
||||
max-height: 50%;
|
||||
max-height: 85%;
|
||||
margin: calc(var(--padding) / 2);
|
||||
}
|
||||
|
||||
|
@ -98,10 +103,13 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-icon.warn-red :global(svg) {
|
||||
.popup-icon :global(svg) {
|
||||
stroke-width: 1.5px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.warn-red :global(svg) {
|
||||
stroke: var(--red);
|
||||
}
|
||||
|
||||
|
@ -119,4 +127,24 @@
|
|||
.popup-title:focus-visible {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.popup-subtext {
|
||||
opacity: 0.7;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.align-left .body-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-left .popup-header {
|
||||
align-items: start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.align-left .popup-icon :global(svg) {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
stroke-width: 1.8px;
|
||||
}
|
||||
</style>
|
||||
|
|
86
web/src/components/settings/CustomInstanceInput.svelte
Normal file
86
web/src/components/settings/CustomInstanceInput.svelte
Normal file
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import { get } from "svelte/store";
|
||||
import { t } from "$lib/i18n/translations";
|
||||
|
||||
import settings, { updateSetting } from "$lib/state/settings";
|
||||
import { customInstanceWarning } from "$lib/api/safety-warning";
|
||||
|
||||
let inputValue = get(settings).processing.customInstanceURL;
|
||||
|
||||
let url: string;
|
||||
let validUrl: boolean;
|
||||
|
||||
const checkUrl = () => {
|
||||
try {
|
||||
let test = /^https:/i.test(new URL(inputValue).protocol);
|
||||
if (test) url = new URL(inputValue).origin.toString();
|
||||
validUrl = true;
|
||||
} catch {
|
||||
validUrl = false;
|
||||
}
|
||||
};
|
||||
|
||||
const writeInput = () => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(inputValue).origin.toString();
|
||||
} catch {
|
||||
return (validUrl = false);
|
||||
}
|
||||
updateSetting({
|
||||
processing: {
|
||||
customInstanceURL: url,
|
||||
},
|
||||
});
|
||||
inputValue = get(settings).processing.customInstanceURL;
|
||||
};
|
||||
</script>
|
||||
|
||||
<input
|
||||
id="link-area"
|
||||
bind:value={inputValue}
|
||||
on:input={() => {
|
||||
checkUrl();
|
||||
}}
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
maxlength="128"
|
||||
placeholder="instance url"
|
||||
/>
|
||||
|
||||
<button
|
||||
id="instance-save"
|
||||
disabled={inputValue == $settings.processing.customInstanceURL || !validUrl}
|
||||
on:click={async () => {
|
||||
await customInstanceWarning();
|
||||
if ($settings.processing.seenCustomWarning) {
|
||||
if (inputValue) writeInput();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t("button.save")}
|
||||
</button>
|
||||
|
||||
{#if $settings.processing.customInstanceURL.length > 0}
|
||||
<button
|
||||
id="instance-reset"
|
||||
on:click={() => {
|
||||
updateSetting({
|
||||
processing: {
|
||||
customInstanceURL: "",
|
||||
},
|
||||
});
|
||||
inputValue = get(settings).processing.customInstanceURL;
|
||||
}}
|
||||
>
|
||||
{$t("button.reset")}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#instance-save[disabled] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
|
||||
export let sectionId: string;
|
||||
export let title: string;
|
||||
export let sectionId: string;
|
||||
|
||||
export let disabled = false;
|
||||
|
||||
let animate = false;
|
||||
|
||||
|
@ -17,6 +19,8 @@
|
|||
id={sectionId}
|
||||
class="settings-content"
|
||||
class:animate
|
||||
class:disabled
|
||||
aria-hidden={disabled}
|
||||
>
|
||||
<h3 class="settings-content-title">{title}</h3>
|
||||
<slot></slot>
|
||||
|
@ -29,6 +33,11 @@
|
|||
gap: var(--padding);
|
||||
padding: calc(var(--settings-padding) / 2);
|
||||
border-radius: 18px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.settings-content.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.settings-content.animate {
|
||||
|
|
|
@ -4,8 +4,16 @@ import env, { apiURL } from "$lib/env";
|
|||
import settings from "$lib/state/settings";
|
||||
|
||||
export const currentApiURL = () => {
|
||||
if (env.DEFAULT_API && get(settings).processing.allowDefaultOverride) {
|
||||
const processingSettings = get(settings).processing;
|
||||
const customInstanceURL = processingSettings.customInstanceURL;
|
||||
|
||||
if (processingSettings.enableCustomInstances && customInstanceURL.length > 0) {
|
||||
return new URL(customInstanceURL).origin;
|
||||
}
|
||||
|
||||
if (env.DEFAULT_API && processingSettings.allowDefaultOverride) {
|
||||
return new URL(env.DEFAULT_API).origin;
|
||||
}
|
||||
|
||||
return new URL(apiURL).origin;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { get } from "svelte/store";
|
|||
import settings from "$lib/state/settings";
|
||||
import { getSession } from "$lib/api/session";
|
||||
import { currentApiURL } from "$lib/api/api-url";
|
||||
import { apiOverrideWarning } from "$lib/api/override-warning";
|
||||
import { apiOverrideWarning } from "$lib/api/safety-warning";
|
||||
|
||||
import type { Optional } from "$lib/types/generic";
|
||||
import type { CobaltAPIResponse, CobaltErrorResponse } from "$lib/types/api";
|
||||
|
|
|
@ -60,3 +60,52 @@ export const apiOverrideWarning = async () => {
|
|||
await promise;
|
||||
}
|
||||
}
|
||||
|
||||
export const customInstanceWarning = async () => {
|
||||
if (env.DEFAULT_API && !get(settings).processing.seenCustomWarning) {
|
||||
let _actions: {
|
||||
resolve: () => void;
|
||||
reject: () => void;
|
||||
};
|
||||
|
||||
const promise = new Promise<void>(
|
||||
(resolve, reject) => (_actions = { resolve, reject })
|
||||
).catch(() => {
|
||||
return {}
|
||||
});
|
||||
|
||||
createDialog({
|
||||
id: "security-api-custom",
|
||||
type: "small",
|
||||
icon: "warn-red",
|
||||
title: get(t)("dialog.safety.title"),
|
||||
bodyText: get(t)("dialog.safety.custom_instance.body"),
|
||||
leftAligned: true,
|
||||
buttons: [
|
||||
{
|
||||
text: get(t)("button.cancel"),
|
||||
main: false,
|
||||
action: () => {
|
||||
_actions.reject();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: get(t)("button.continue"),
|
||||
color: "red",
|
||||
main: true,
|
||||
timeout: 15000,
|
||||
action: () => {
|
||||
_actions.resolve();
|
||||
updateSetting({
|
||||
processing: {
|
||||
seenCustomWarning: true,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await promise;
|
||||
}
|
||||
}
|
|
@ -33,7 +33,10 @@ const defaultSettings: CobaltSettings = {
|
|||
},
|
||||
processing: {
|
||||
allowDefaultOverride: false,
|
||||
customInstanceURL: "",
|
||||
enableCustomInstances: false,
|
||||
seenOverrideWarning: false,
|
||||
seenCustomWarning: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ type SmallDialog = Dialog & {
|
|||
bodyText?: string,
|
||||
bodySubText?: string,
|
||||
buttons?: DialogButton[],
|
||||
leftAligned?: boolean,
|
||||
};
|
||||
|
||||
type PickerDialog = Dialog & {
|
||||
|
|
|
@ -28,6 +28,9 @@ type CobaltSettingsPrivacy = {
|
|||
|
||||
type CobaltSettingsProcessing = {
|
||||
allowDefaultOverride: boolean,
|
||||
customInstanceURL: string,
|
||||
enableCustomInstances: boolean,
|
||||
seenCustomWarning: boolean,
|
||||
seenOverrideWarning: boolean,
|
||||
}
|
||||
|
||||
|
|
|
@ -10,14 +10,15 @@
|
|||
import SettingsNavSection from "$components/settings/SettingsNavSection.svelte";
|
||||
|
||||
import IconSunHigh from "@tabler/icons-svelte/IconSunHigh.svelte";
|
||||
import IconLock from "@tabler/icons-svelte/IconLock.svelte";
|
||||
|
||||
import IconMovie from "@tabler/icons-svelte/IconMovie.svelte";
|
||||
import IconMusic from "@tabler/icons-svelte/IconMusic.svelte";
|
||||
import IconFileDownload from "@tabler/icons-svelte/IconFileDownload.svelte";
|
||||
import IconSettingsBolt from "@tabler/icons-svelte/IconSettingsBolt.svelte";
|
||||
|
||||
import IconBug from "@tabler/icons-svelte/IconBug.svelte";
|
||||
import IconLock from "@tabler/icons-svelte/IconLock.svelte";
|
||||
import IconCloudNetwork from "@tabler/icons-svelte/IconCloudNetwork.svelte";
|
||||
import IconWorld from "@tabler/icons-svelte/IconWorld.svelte";
|
||||
import IconSettingsBolt from "@tabler/icons-svelte/IconSettingsBolt.svelte";
|
||||
|
||||
import IconArrowLeft from "@tabler/icons-svelte/IconArrowLeft.svelte";
|
||||
|
||||
|
@ -131,11 +132,11 @@
|
|||
|
||||
<SettingsNavSection>
|
||||
<SettingsNavTab
|
||||
tabName="processing"
|
||||
tabLink="processing"
|
||||
tabName="instances"
|
||||
tabLink="instances"
|
||||
iconColor="gray"
|
||||
>
|
||||
<IconCloudNetwork />
|
||||
<IconWorld />
|
||||
</SettingsNavTab>
|
||||
<SettingsNavTab
|
||||
tabName="advanced"
|
||||
|
|
43
web/src/routes/settings/instances/+page.svelte
Normal file
43
web/src/routes/settings/instances/+page.svelte
Normal file
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import env from "$lib/env";
|
||||
import settings from "$lib/state/settings";
|
||||
|
||||
import { t } from "$lib/i18n/translations";
|
||||
|
||||
import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
|
||||
import SettingsCategory from "$components/settings/SettingsCategory.svelte";
|
||||
import CustomInstanceInput from "$components/settings/CustomInstanceInput.svelte";
|
||||
|
||||
$: overrideDisabled = $settings.processing.enableCustomInstances;
|
||||
</script>
|
||||
|
||||
{#if env.DEFAULT_API}
|
||||
<SettingsCategory
|
||||
sectionId="override"
|
||||
title={$t("settings.processing.override")}
|
||||
disabled={overrideDisabled}
|
||||
>
|
||||
<SettingsToggle
|
||||
settingContext="processing"
|
||||
settingId="allowDefaultOverride"
|
||||
title={$t("settings.processing.override.title")}
|
||||
description={$t("settings.processing.override.description")}
|
||||
disabled={overrideDisabled}
|
||||
/>
|
||||
</SettingsCategory>
|
||||
{/if}
|
||||
|
||||
<SettingsCategory
|
||||
sectionId="community"
|
||||
title={$t("settings.processing.community")}
|
||||
>
|
||||
<SettingsToggle
|
||||
settingContext="processing"
|
||||
settingId="enableCustomInstances"
|
||||
title={$t("settings.processing.enable_custom.title")}
|
||||
description={$t("settings.processing.enable_custom.description")}
|
||||
/>
|
||||
{#if $settings.processing.enableCustomInstances}
|
||||
<CustomInstanceInput />
|
||||
{/if}
|
||||
</SettingsCategory>
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">
|
||||
import env from "$lib/env";
|
||||
import { t } from "$lib/i18n/translations";
|
||||
|
||||
import SettingsToggle from "$components/buttons/SettingsToggle.svelte";
|
||||
import SettingsCategory from "$components/settings/SettingsCategory.svelte";
|
||||
</script>
|
||||
|
||||
{#if env.DEFAULT_API}
|
||||
<SettingsCategory sectionId="override" title={$t("settings.processing.override")}>
|
||||
<SettingsToggle
|
||||
settingContext="processing"
|
||||
settingId="allowDefaultOverride"
|
||||
title={$t("settings.processing.override.title")}
|
||||
description={$t("settings.processing.override.description")}
|
||||
/>
|
||||
</SettingsCategory>
|
||||
{/if}
|
Loading…
Reference in a new issue