refactors and a stupid animation

This commit is contained in:
tezlm 2023-07-09 12:30:09 -07:00
parent e1c243bbbc
commit b2030047a3
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
12 changed files with 150 additions and 31 deletions

View file

@ -22,7 +22,7 @@ struct Config {
#[derive(Debug, Serialize, Deserialize)]
struct ConfigProfile {
base_url: String, // TODO: use base_url in requests
base_url: String,
key: ActorSecret,
token: String,
}
@ -101,7 +101,6 @@ async fn main() -> Result<(), Error> {
Item::Blob(blob) => println!("blob ({} bytes)", blob.len()),
};
} else {
// TODO: fix this mess
let relations: Option<HashSet<_, _>> = rels
.into_iter()
.map(|i| i.split_once("/").map(|i| (i.0.to_string(), i.1.to_string())))

View file

@ -26,7 +26,13 @@ pub enum ActorIdParseError {
#[derive(Debug, PartialEq, Eq, Clone, Error)]
pub enum ActorSecretParseError {
#[error("invalid base64")] InvalidBase64,
#[error("incorrect byte count (should be 32)")] IncorrectByteCount,
#[error("incorrect byte count (should be 64)")] IncorrectByteCount,
}
#[derive(Debug, PartialEq, Eq, Clone, Error)]
pub enum ActorSignatureParseError {
#[error("invalid base64")] InvalidBase64,
#[error("incorrect byte count (should be 64)")] IncorrectByteCount,
}
impl ActorId {
@ -177,17 +183,17 @@ impl<'de> Deserialize<'de> for ActorId {
}
impl FromStr for ActorSignature {
type Err = &'static str; // TODO: proper errors
type Err = ActorSignatureParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let key = b64engine.decode(s).map_err(|_| "invalid base64")?;
let bytes = key.try_into().map_err(|_| "incorrect byte count")?;
let key = b64engine.decode(s).map_err(|_| ActorSignatureParseError::InvalidBase64)?;
let bytes = key.try_into().map_err(|_| ActorSignatureParseError::IncorrectByteCount)?;
Ok(Self(bytes))
}
}
impl TryFrom<&str> for ActorSignature {
type Error = &'static str;
type Error = ActorSignatureParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)

View file

@ -17,4 +17,5 @@ pub trait Database {
async fn create_event(&self, event: &Event) -> Result<(), Self::Error>;
async fn get_event(&self, item_ref: &ItemRef) -> Result<Event, Self::Error>;
async fn query_events(&self, query: &Query, limit: Option<u32>, after: Option<String>) -> Result<QueryResult, Self::Error>;
// async fn query_relations(&self, query: &Vec<(String, String)>) -> Result<HashMap<ItemRef, Event>, Self::Error>;
}

View file

@ -77,11 +77,9 @@ pub async fn route(
};
// relations query
dbg!(&query);
let relations = if !query.relations.is_empty() && !result.events.is_empty() {
// FIXME: events_to and events_from seem to be swapped???? i should really fix this
let mut builder = sqlx::QueryBuilder::new("
SELECT relations.ref_to AS item_ref, events_from.json AS json
SELECT relations.ref_from AS item_ref, events_from.json AS json
FROM relations
JOIN events AS events_from ON events_from.ref = relations.ref_from
JOIN events AS events_to ON events_to.ref = relations.ref_to

View file

@ -26,7 +26,7 @@ pub mod perms {
impl AuthLevel for ReadOnly { fn new() -> Self { Self } fn to_num(&self) -> u8 { 1 } }
impl AuthLevel for ReadWrite { fn new() -> Self { Self } fn to_num(&self) -> u8 { 2 } }
impl AuthLevel for Everything { fn new() -> Self { Self } fn to_num(&self) -> u8 { 3 } }
impl AuthLevel for Admin { fn new() -> Self { Self } fn to_num(&self) -> u8 { 4 } }
impl AuthLevel for Admin { fn new() -> Self { Self } fn to_num(&self) -> u8 { 4 } }
}
pub struct Authenticate<T: perms::AuthLevel> {

View file

@ -24,6 +24,16 @@
}
}
// this always bothered me, but there was never a good way to sync animations... until now
const syncedAnims = new Set(["hover"]);
function syncAnimations(e: AnimationEvent) {
for (let anim of e.target.getAnimations()) {
if (syncedAnims.has(anim.name)) {
anim.startTime = 0;
}
}
}
window.addEventListener("hashchange", setScene);
window.addEventListener("load", setScene);
</script>
@ -41,3 +51,4 @@
<svelte:component this={selected} {options} />
</main>
</div>
<svelte:window on:animationstart={syncAnimations} />

5
web/animations.css Normal file
View file

@ -0,0 +1,5 @@
@keyframes hover {
0% { transform: translateY(-1px) }
50% { transform: translateY(4px) }
100% { transform: translateY(-1px) }
}

View file

@ -1,9 +1,9 @@
<svelte:options immutable />
<script lang="ts">
import { onDestroy } from "svelte";
import brokenImageUrl from "./broken.png";
import { api } from "./lib/api";
import type { Event } from "./lib/api";
import brokenImageUrl from "../broken.png";
import { api } from "../lib/api";
import type { Event } from "../lib/api";
export let event: Event;
export let style = "";
export let thumb: null | [number, number] = null;
@ -13,11 +13,10 @@
onDestroy(() => url.then(url => URL.revokeObjectURL(url)));
</script>
{#await url}
<img title="{event.content.name}" alt="{event.content.name}" {style} />
<img alt="{event.content.name}" {style} />
{:then url}
<img
src={url}
title="{event.content.name}"
alt="{event.content.name}"
{style}
on:error={e => e.target.src = brokenImageUrl}
@ -25,6 +24,7 @@
{/await}
<style>
img {
display: block;
height: 100%;
width: 100%;
object-fit: cover;

23
web/atoms/Text.svelte Normal file
View file

@ -0,0 +1,23 @@
<svelte:options immutable />
<script lang="ts">
import { onDestroy } from "svelte";
import { api } from "../lib/api";
import type { Event } from "../lib/api";
export let event: Event;
export let style = "";
let text = api.fetchBlob(event.id).then(b => b.text());
let blob = text.then(t => new Blob([t], { type: "text/plain" }));
export let url = blob.then(blob => URL.createObjectURL(blob));
onDestroy(() => url.then(url => URL.revokeObjectURL(url)));
</script>
<pre>{#await text then content}{content}{/await}</pre>
<style>
img {
display: block;
height: 100%;
width: 100%;
object-fit: cover;
background: #2a2a33;
}
</style>

View file

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Thing</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="animations.css" />
</head>
<body>
<noscript>this needs js</noscript>

View file

@ -1,15 +1,16 @@
<script lang="ts">
import brokenImageUrl from "../broken.png";
import { api } from "../lib/api";
import { collectEvents } from "../lib/util";
import { quadOut } from "svelte/easing";
import { onDestroy, onMount } from "svelte";
import Img from "../Img.svelte";
import Img from "../atoms/Img.svelte";
import Text from "../atoms/Text.svelte";
export let options: URLSearchParams;
let { events: items, stop } = loadImages(options);
let popup = null;
let galleryEl: HTMLDivElement;
let tooltip = { x: 0, y: 0, text: null };
$: if (options) {
stop();
@ -66,12 +67,24 @@
};
}
function handleMouseover(e: MouseEvent) {
tooltip = {
x: e.clientX,
y: e.clientY,
text: e.target.alt || e.target.dataset.fileName,
};
}
function handleMouseout(e: MouseEvent) {
tooltip.text = null;
}
onDestroy(() => stop());
let searchRaw = "";
$: search = searchRaw.toLocaleLowerCase();
$: mapped = $items.map(event => ({ event, info: event.derived.file ?? {} }));
$: filtered = mapped.filter(({ info, event }) => info.mime.startsWith("image/") && event.content.name?.includes(search));
$: filtered = mapped.filter(({ info, event }) => event.content.name?.includes(search));
</script>
<div>
<div style="margin-bottom: 8px">
@ -89,21 +102,48 @@
-
<input placeholder="Search filenames..." bind:value={searchRaw}>
</div>
<div class="gallery" bind:this={galleryEl}>
<div
class="gallery"
bind:this={galleryEl}
on:mouseover={handleMouseover}
on:mousemove={handleMouseover}
on:mouseout={handleMouseout}
>
{#each filtered as { event, info } (event.id)}
<div class="item" on:click|preventDefault={() => popup = { event }}>
{#if info.mime.startsWith("image/")}
<div on:click|preventDefault={() => popup = { event }}>
<Img {event} thumb={[100, 100]} />
</div>
<Img {event} thumb={[100, 100]} />
{:else}
<div style="padding: 4px; white-space: normal; height: 100%" data-file-name={event.content.name}>{event.content.name}</div>
{/if}
<div class="name">{event.content.name}</div>
</div>
{/each}
</div>
<div
class="tooltip"
style:display={tooltip.text ? "block" : "none"}
style:translate="{tooltip.x}px {tooltip.y}px"
>
{tooltip.text}
</div>
{#if popup}
<div class="popup" on:click={() => popup = null} transition:opacity>
{@const event = popup.event}
{@const info = event.derived.file ?? {}}
<div class="popup" on:click={() => popup = null} transition:opacity data-event-id={event.id}>
<div on:click|stopPropagation transition:pop role="dialog">
<Img event={popup.event} style="max-width: 80vw; max-height: 80vh" bind:url={popup.url} />
<div style="border-radius: 4px; overflow: hidden;">
{#if info.mime.startsWith("image/")}
<Img event={event} style="max-width: 80vw; max-height: 80vh" bind:url={popup.url} />
{:else}
<div class="text">
<Text event={event} bind:url={popup.url} />
</div>
{/if}
</div>
{#await popup.url then url}
<a target="_blank" href={url}>Open in new tab</a> <span class="eventid">{popup.event.id}</span>
<!-- <a target="_blank" href={url} download={event.content.name}>Open in new tab</a> -->
<a target="_blank" href={url}>Open in new tab</a>
{/await}
</div>
</div>
@ -114,23 +154,43 @@
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
margin: 0 auto;
gap: 4px;
}
.gallery div {
.item {
position: relative;
overflow: hidden;
width: 100%;
height: 100px;
background-color: #2a2a33;
cursor: pointer;
border-radius: 4px;
}
.gallery img {
.item .name {
display: none;
position: absolute;
width: 100%;
height: 100%;
padding: 4px;
background-color: #000000aa;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
bottom: 0;
}
img {
background: #2a2a33;
.tooltip {
position: absolute;
top: 0;
left: 0;
background: var(--bg-primary);
border: solid var(--borders) 1px;
border-radius: 4px;
padding: 4px;
pointer-events: none;
animation: hover 1.5s ease-in-out infinite;
transform-origin: top left;
box-shadow: #00000055 4px 4px 4px;
}
.popup {
@ -164,4 +224,15 @@
.popup .eventid {
user-select: all;
}
.popup .text {
background: #2a2a33;
border-radius: 4px;
white-space: normal;
max-width: 80vw;
max-height: 80vh;
overflow: auto;
font-family: monospace;
padding: 8px;
}
</style>

View file

@ -14,6 +14,10 @@
--borders: #222a30;
}
body {
overflow: hidden;
}
h1 { font-size: 2em }
h2 { font-size: 1.5em }
h3 { font-size: 1.3em }