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:
wukko 2024-08-30 17:15:05 +06:00
parent 70c1a85766
commit 33d6b5bd81
No known key found for this signature in database
GPG key ID: 3E30B3F26C7B4AA2
16 changed files with 278 additions and 39 deletions

View file

@ -11,5 +11,6 @@
"import": "import",
"continue": "continue",
"star": "star",
"follow": "follow"
"follow": "follow",
"save": "save"
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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 {

View file

@ -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>

View 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>

View file

@ -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 {

View file

@ -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;
}

View file

@ -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";

View file

@ -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;
}
}

View file

@ -33,7 +33,10 @@ const defaultSettings: CobaltSettings = {
},
processing: {
allowDefaultOverride: false,
customInstanceURL: "",
enableCustomInstances: false,
seenOverrideWarning: false,
seenCustomWarning: false,
}
}

View file

@ -29,6 +29,7 @@ type SmallDialog = Dialog & {
bodyText?: string,
bodySubText?: string,
buttons?: DialogButton[],
leftAligned?: boolean,
};
type PickerDialog = Dialog & {

View file

@ -28,6 +28,9 @@ type CobaltSettingsPrivacy = {
type CobaltSettingsProcessing = {
allowDefaultOverride: boolean,
customInstanceURL: string,
enableCustomInstances: boolean,
seenCustomWarning: boolean,
seenOverrideWarning: boolean,
}

View file

@ -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"

View 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>

View file

@ -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}