upend/webext/src/App.svelte

391 lines
8.7 KiB
Svelte

<script lang="ts">
import browser from "webextension-polyfill";
import { UpEndApi } from "upend/api";
import { cleanInstanceUrl, instanceUrlStore } from "./common";
import { onMount } from "svelte";
import "./main.scss";
const api = new UpEndApi("http://localhost:8093");
let visitngUpEnd = false;
let opening = false;
let openError: string | undefined;
let instanceUrl: string;
$: instanceUrl = $instanceUrlStore;
let instanceUrlModified = false;
$: instanceUrlModified = $instanceUrlStore !== instanceUrl;
let instanceVersion: string;
let instanceVersionError: string;
$: Boolean($instanceUrlStore) && fetchVersion();
async function fetchVersion() {
instanceVersion = undefined;
instanceVersionError = undefined;
try {
const vaultInfo = await api.fetchInfo();
instanceVersion = vaultInfo.version;
} catch (err: unknown) {
instanceVersionError = processError(err);
}
}
async function getCurrentUrl() {
const currentTab = (
await browser.tabs.query({
active: true,
currentWindow: true,
})
)[0];
return currentTab.url;
}
let currentUrl: string | undefined;
let contentType: string | undefined;
onMount(async () => {
browser.tabs
.executeScript(undefined, {
code: `document.querySelector('meta[name="application-name"]')?.content`,
})
.then((result) => {
visitngUpEnd = (result[0] as string | undefined) === "UpEnd";
});
currentUrl = await getCurrentUrl();
browser.tabs.onUpdated.addListener(async () => {
if (currentUrl !== (await getCurrentUrl())) {
window.close();
}
});
contentType = (await browser.tabs.executeScript(undefined, {
code: "document.contentType",
})) as unknown as string | undefined;
});
enum PRIMARY_TYPE {
URL = "URL",
CONTENT = "Content",
UNKNOWN = "???",
}
let primaryType = PRIMARY_TYPE.UNKNOWN;
$: primaryType = !contentType
? PRIMARY_TYPE.UNKNOWN
: contentType == "text/html"
? PRIMARY_TYPE.URL
: PRIMARY_TYPE.CONTENT;
enum PRIMARY_ACTION {
OPEN = "Open",
SAVE = "Save",
UNKNOWN = "???",
}
let primaryAction = PRIMARY_ACTION.UNKNOWN;
$: {
if (primaryType !== PRIMARY_TYPE.UNKNOWN) {
let count =
primaryType == PRIMARY_TYPE.URL ? urlEntriesCount : contentEntriesCount;
if (count !== undefined) {
primaryAction = count > 0 ? PRIMARY_ACTION.OPEN : PRIMARY_ACTION.SAVE;
}
}
}
let urlAddress: string | undefined = undefined;
let contentAddress: string | undefined = undefined;
$: currentUrl &&
api
.getAddress({ url: currentUrl })
.then((address) => {
urlAddress = address;
})
.catch((err) => {
console.error(err);
urlAddress = undefined;
});
$: currentUrl &&
api
.getAddress({ urlContent: currentUrl })
.then((address) => (contentAddress = address))
.catch((err) => {
console.error(err);
contentAddress = undefined;
});
let urlEntriesCount: number | undefined = undefined;
let contentEntriesCount: number | undefined = undefined;
let urlAdded: Date | undefined;
let contentAdded: Date | undefined;
$: urlAddress &&
api
.fetchEntity(urlAddress)
.then((obj) => {
urlEntriesCount = Object.keys(obj.listing.entries).length;
let added = obj.get("ADDED");
urlAdded =
typeof added === "number" ? new Date(added * 1000) : undefined;
})
.catch((err) => {
console.error(err);
urlEntriesCount = undefined;
urlAdded = undefined;
});
$: contentAddress &&
api
.fetchEntity(contentAddress)
.then((obj) => {
contentEntriesCount = Object.keys(obj.listing.entries).length;
let added = obj.get("ADDED");
contentAdded =
typeof added === "number" ? new Date(added * 1000) : undefined;
})
.catch((err) => {
console.error(err);
contentEntriesCount = undefined;
});
async function performPrimary() {
if (
primaryType == PRIMARY_TYPE.UNKNOWN ||
primaryAction == PRIMARY_ACTION.UNKNOWN
) {
return;
}
switch (primaryType) {
case PRIMARY_TYPE.URL:
await performUrl();
break;
case PRIMARY_TYPE.CONTENT:
await performContent();
break;
}
}
async function performUrl() {
if (urlEntriesCount === undefined) {
return;
}
if (urlEntriesCount === 0) {
await api.putEntry({
entity: {
t: "Url",
c: currentUrl,
},
});
}
visitAddress(urlAddress);
}
async function performContent() {
if (contentEntriesCount === undefined) {
return;
}
if (contentEntriesCount === 0) {
await api.putBlob(new URL(currentUrl));
}
visitAddress(contentAddress);
}
function visitAddress(address: string) {
browser.tabs.create({ url: `${$cleanInstanceUrl}/#/browse/${address}` });
window.close();
}
function processError(err: unknown): string {
if (err instanceof Error) {
if (err.message.includes("NetworkError")) {
return "Network Error. Is UpEnd running?";
} else {
return err.message;
}
} else {
return String(err);
}
}
</script>
<main>
{#if !visitngUpEnd}
<div class="primary-controls">
<button
class="button"
disabled={primaryAction == PRIMARY_ACTION.UNKNOWN}
on:click={performPrimary}
>
{primaryAction} as {primaryType}
</button>
<div class="sublabel">Content type: {contentType || "???"}</div>
</div>
<div class="controls row">
<div class="labeled-button">
<button
class="button"
disabled={urlEntriesCount === undefined}
on:click={performUrl}
>
{urlEntriesCount === undefined
? "???"
: urlEntriesCount > 0
? "Open"
: "Save"} URL
</button>
<div class="sublabel">
{#if urlAdded}
Added at {urlAdded.toLocaleString()}
{:else}
---
{/if}
</div>
</div>
<div class="labeled-button">
<button
class="button"
disabled={contentEntriesCount === undefined}
on:click={performContent}
>
{contentEntriesCount === undefined
? "???"
: contentEntriesCount > 0
? "Open"
: "Save"} Content
</button>
<div class="sublabel">
{#if contentAdded}
Added at {contentAdded.toLocaleString()}
{:else}
---
{/if}
</div>
</div>
</div>
{#if opening && !openError}
<div class="status-label">Working, please wait...</div>
{/if}
{#if openError}
<div class="status-label error">{openError}</div>
{/if}
<hr />
<div class="row">
<label>
Instance URL
<input
class="instance-input"
type="url"
bind:value={instanceUrl}
class:modified={instanceUrlModified}
/>
</label>
<button class="button" on:click={() => ($instanceUrlStore = instanceUrl)}>
Save
</button>
</div>
<div class="version">
Status: {#if !instanceVersionError}
{`OK, v.${instanceVersion}` || "???"}
{:else}
<div class="error">{instanceVersionError}</div>
{/if}
</div>
{:else}
<div class="visitingUpEnd">You rock :)</div>
{/if}
</main>
<style lang="scss">
@use "../../webui/src/styles/colors";
main {
padding: 1em;
}
input {
background: var(--background);
color: var(--foreground);
border: 1px solid var(--foreground);
border-radius: 2px;
}
input[type="url"] {
font-family: var(--monospace-font);
&:focus-visible {
outline: 1px solid var(--primary-lighter);
}
&:invalid {
color: colors.$red;
outline: 2px solid colors.$red;
}
}
.instance-input.modified {
color: colors.$yellow;
}
hr {
margin: 1rem 0;
}
.controls {
display: flex;
justify-content: space-evenly;
}
.primary-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
margin-bottom: 1rem;
.button {
font-size: 1.5rem;
}
}
.row {
display: flex;
align-items: center;
gap: 1rem;
}
.labeled-button {
display: flex;
flex-direction: column;
align-items: center;
}
.status-label {
margin-top: 1rem;
text-align: center;
}
.sublabel {
font-size: 0.75rem;
}
.error {
color: colors.$red;
}
.version .error {
display: inline;
}
</style>