Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
},
"tauri": {
"allowlist": {
"all": true
"all": true,
"clipboard": {
"all": true,
"writeText": true,
"readText": true
}
},
"bundle": {
"active": true,
Expand Down
34 changes: 31 additions & 3 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@
import { SvelteToast } from '@zerodevx/svelte-toast';
import { readCurrentLinks } from './components/utils';
import Loading from '~icons/line-md/loading-loop';
import { readText } from '@tauri-apps/api/clipboard';

let initialFetch: Promise<LinkInfo[]>;
let showForm = false;
let url = '';

async function onKeyDown(event: KeyboardEvent) {
// Open save dialog if ctrl+v is used but only if it's not already open
// It then auto-fills the url field with clipboard contents
if (event.ctrlKey && event.key == 'v' && !showForm) {
url = await readText() ?? "";
showForm = !showForm;
}
}

onMount(() => {
initialFetch = readCurrentLinks();
Expand All @@ -19,8 +31,16 @@
</script>

<div class="relative flex h-screen min-h-screen w-screen flex-col text-white">
<h1 class="fixed flex h-14 w-full items-center bg-neutral-800 px-8 text-lg font-medium">
ARK Shelf
<h1 class="fixed flex h-14 w-full items-center justify-between bg-neutral-800 px-8 text-lg font-medium">
ARK Shelf
<!-- Show Link Creation Form -->
<button on:click={() => (showForm = !showForm)} class="text-white px-4 py-2 rounded-md ml-4 border hover:bg-blue-400 border-blue-400">
{#if showForm}
Hide
{:else}
Save
{/if}
</button>
</h1>
<main class="absolute top-14 h-[calc(100vh-3.5rem)] w-screen">
<div class="flex h-full overflow-hidden overflow-y-scroll bg-neutral-950 px-8 py-4">
Expand All @@ -36,7 +56,13 @@
{/each}
{/await}
</div>
<Form />

<!-- Clicking Save button will open the link creation form
Clicking Hide or sucessfully submitting the link will close it.
If opened via ctrl+v then the meta fields will also open -->
{#if showForm}
<Form url={url} bind:show={showForm}/>
{/if}
</div>
</main>
</div>
Expand All @@ -54,3 +80,5 @@
--toastContainerLeft: auto;
}
</style>

<svelte:window on:keydown={onKeyDown} />
113 changes: 59 additions & 54 deletions src/components/Form.svelte
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
<script lang="ts">
import { toast } from '@zerodevx/svelte-toast';
import { linksInfos } from '../store';
import { createLink, debounce, getPreview } from './utils';
import { createLink, getPreview } from './utils';
import Calendar from '~icons/ic/baseline-calendar-month';
import Scores from '~icons/ic/baseline-format-list-bulleted';

let url = '';
let title = '';
let description = '';
const mode = linksInfos.mode;

$: disabled = !url;
// Used by App.svelte
export let url = '';
export let show = false;

const auto = async () => {
if (url && title && description) {
return;
} else if (url) {
let showMeta = false;
let inputWaitTime = 1500
let timer;
let titleElement: HTMLInputElement;
let descriptionElement: HTMLInputElement;

// Waits inputWaitTime after every new keypress into the URL form before trying for a preview and open the meta fields
// This might seem like a lot but most people type with staggered delays, 1500ms as a starting point seems a decent compromise
// If delay time is hit with an invalid url,then increase inputWaitTime by 750ms to accomodate slower typing and reduce notifications.
const debounce = async () => {
clearTimeout(timer);
timer = setTimeout( async () => {
showMeta = true;
const graph = await getPreview(url);
if (graph) {
title = graph.title ?? '';
description = graph.description ?? '';
titleElement.value = graph.title ?? '';
descriptionElement.value = graph.description ?? '';
} else {
inputWaitTime += 750
toast.push('Failed to fetch website data');
}
}
};

let error = false;

const debouncedCheck = debounce((url: string) => {
if ($linksInfos.some(l => l.url === url)) {
error = true;
} else {
error = false;
}
}, 200);
}, inputWaitTime);
}
</script>

<div class="w-56">
<div class="flex w-full justify-between">
<!-- Sort by Calendar Button -->
<button
class="rounded-md p-2"
class:bg-green-400={$mode === 'date'}
Expand All @@ -47,6 +47,8 @@
}}
><Calendar />
</button>

<!-- Sort by Score button -->
<button
class="rounded-md p-2"
class:bg-green-400={$mode === 'score'}
Expand All @@ -56,6 +58,8 @@
<Scores />
</button>
</div>

<!-- Link Creation Form -->
<form
class="sticky top-0 flex flex-col space-y-2"
on:submit|preventDefault={async e => {
Expand All @@ -69,11 +73,15 @@
url,
desc,
};
if ($linksInfos.every(l => l.url !== url)) {
if ($linksInfos.some(link => link.url != url)) {
toast.push("There is already a link with the same URL")
return
}
if ($linksInfos.every(link => link.url !== url)) {
const newLink = await createLink(data);
if (newLink) {
linksInfos.update(links => {
links = links.filter(l => l.url !== url);
links = links.filter(link => link.url !== url);
links.push(newLink);
return links;
});
Expand All @@ -82,56 +90,53 @@
} else {
toast.push('Error creating link');
}
show = false;
}
}}>

<!-- URL Field -->
<label for="url" aria-label="URL" />
{#if error}
<p class="break-words text-red-500">There is already a link with the same URL</p>
{/if}
<input
type="text"
id="url"
name="url"
required
placeholder="URL*"
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
on:keyup={e => {
debouncedCheck(e.currentTarget.value);
}}
on:change={e => {
debouncedCheck(e.currentTarget.value);
}}
bind:value={url}
on:paste|preventDefault={e => {
const text = e.clipboardData?.getData('text');
if (text) {
url = text;
description = '';
title = '';
}
}} />
<label for="title" aria-label="Title" />
<input
on:input={debounce}
/>

<!--
Meta Fields
If the input url debounce timeout has been triggered, show the title and description fields
Title is autopopulated if possible
-->
{#if showMeta}
<label for="title" aria-label="Title" />
<input
type="text"
id="title"
name="title"
required
placeholder="Title*"
bind:value={title}
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500" />
<label for="description" aria-label="Optional description" />
<input
bind:this={titleElement}
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
/>
<label for="description" aria-label="Optional description" />
<input
type="text"
name="description"
bind:value={description}
bind:this={descriptionElement}
placeholder="Description (Optional)"
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
id="description" />
id="description"
/>
{/if}

<!-- Create Button -->
<div class="flex justify-between">
<button type="submit" class="pl-2 text-blue-400" disabled={error}>CREATE</button>
<button class="pr-2 text-rose-700" {disabled} type="button" on:click={auto}>
AUTO FILLED
</button>
<button type="submit" class="pl-2 text-blue-400">CREATE</button>
</div>
</form>
</div>
12 changes: 0 additions & 12 deletions src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,3 @@ export const createScore = async ({ value, url }: { value: number; url: string }
return;
}
};

export const debounce = (callback: unknown, wait = 500) => {
let timeoutId: number;
return (...args: unknown[]) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
if (typeof callback === 'function') {
callback(...args);
}
}, wait);
};
};