398 lines
8.8 KiB
Svelte
398 lines
8.8 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";
|
|
|
|
$: api = new UpEndApi($instanceUrlStore);
|
|
|
|
let visitingUpEnd = 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 () => {
|
|
visitingUpEnd =
|
|
(
|
|
await browser.tabs.executeScript(undefined, {
|
|
code: `document.querySelector('meta[name="application-name"]')?.content`,
|
|
})
|
|
)[0] === "UpEnd";
|
|
|
|
currentUrl = await getCurrentUrl();
|
|
|
|
browser.tabs.onUpdated.addListener(async () => {
|
|
if (currentUrl !== (await getCurrentUrl())) {
|
|
window.close();
|
|
}
|
|
});
|
|
|
|
contentType = (
|
|
await browser.tabs.executeScript(undefined, {
|
|
code: "document.contentType",
|
|
})
|
|
)[0];
|
|
});
|
|
|
|
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 !visitingUpEnd}
|
|
<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>
|
|
<a href={$instanceUrlStore} target="_blank"> Instance </a>
|
|
<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">
|
|
UpEnd Companion is (yet?) useless while visiting an UpEnd instance.
|
|
<br />By the way, 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;
|
|
}
|
|
|
|
.visitingUpEnd {
|
|
text-align: center;
|
|
}
|
|
</style>
|