Skip to content
Draft
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
15 changes: 4 additions & 11 deletions web/src/lib/components/Nip94.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { Event } from 'nostr-tools';
import Img from './content/Img.svelte';

interface Props {
event: Event;
Expand All @@ -19,18 +20,10 @@
);
</script>

{#if mimeType !== undefined && /image\/(gif|jpg|jpeg|png|webp|bmp)/.test(mimeType)}
{#if url !== undefined && URL.canParse(url) && mimeType !== undefined && /image\/(avif|gif|jpg|jpeg|png|webp|bmp|apng)/.test(mimeType)}
<a href={url} target="_blank" rel="noopener noreferrer">
<img src={url} alt={event.content} />
<Img url={new URL(url)} {mimeType} />
</a>
{:else}
{:else if url !== undefined}
<a href={url} target="_blank" rel="noopener noreferrer">{url}</a>
{/if}

<style>
img {
max-width: 100%;
max-height: 20em;
margin: 0.5em;
}
</style>
152 changes: 152 additions & 0 deletions web/src/lib/components/content/FreezeframeImage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<script lang="ts">
import { createFreezeframeImageState } from '$lib/media/FreezeframeImageState.svelte';

interface Props {
src: string;
alt: string;
blur?: boolean;
}

let { src, alt, blur = false }: Props = $props();

let canvas: HTMLCanvasElement | undefined = $state();
const freezeframe = createFreezeframeImageState({
canvas: () => canvas,
src: () => src
});
</script>

<span
class="freezeframe-image"
onpointerenter={freezeframe.pointerEnter}
onpointerleave={freezeframe.pointerLeave}
role="group"
aria-label={alt}
>
<canvas
class="global-content-image freezeframe-canvas"
class:playing={freezeframe.playing}
class:blur
hidden={freezeframe.failed}
bind:this={canvas}
aria-hidden="true"
></canvas>
{#if freezeframe.failed}
<img class="global-content-image" class:blur {src} alt="" aria-hidden="true" />
{:else if freezeframe.playerSrc !== undefined}
<img
class="global-content-image freezeframe-player"
class:playing={freezeframe.playing}
class:blur
src={freezeframe.playerSrc}
alt=""
aria-hidden="true"
/>
{/if}
{#if freezeframe.ready && !freezeframe.failed && (!freezeframe.playing || freezeframe.touchControl)}
<span
class="freezeframe-control"
class:playing={freezeframe.playing}
onpointerdown={freezeframe.controlPointerDown}
onclick={freezeframe.controlClick}
onkeydown={freezeframe.controlKeydown}
role="button"
tabindex="0"
aria-label={freezeframe.playing ? 'Stop animation' : 'Play animation'}
aria-pressed={freezeframe.playing}
></span>
{/if}
</span>

<style>
.freezeframe-image {
position: relative;
display: inline-grid;
grid-template-areas: 'image';
max-width: 100%;
line-height: 0;
vertical-align: middle;
}

.freezeframe-canvas,
.freezeframe-player {
grid-area: image;
}

.freezeframe-canvas {
display: block;
box-sizing: border-box;
object-fit: contain;
}

.freezeframe-canvas.playing {
opacity: 0;
}

.freezeframe-player {
display: block;
box-sizing: border-box;
object-fit: contain;
opacity: 0;
pointer-events: none;
}

.freezeframe-image .freezeframe-player {
border-color: transparent;
}

.freezeframe-player.playing {
opacity: 1;
}

.freezeframe-image .freezeframe-player.playing {
border-color: lightgray;
}

.freezeframe-control {
position: absolute;
top: 50%;
left: 50%;
width: 3em;
height: 3em;
transform: translate(-50%, -50%);
border: 1px solid rgb(255 255 255 / 80%);
border-radius: 50%;
background: rgb(0 0 0 / 55%);
box-shadow: 0 2px 8px rgb(0 0 0 / 25%);
cursor: pointer;
}

.freezeframe-control::before {
position: absolute;
top: 50%;
left: 54%;
width: 0;
height: 0;
transform: translate(-50%, -50%);
border-top: 0.65em solid transparent;
border-bottom: 0.65em solid transparent;
border-left: 1em solid white;
content: '';
}

.freezeframe-control.playing::before,
.freezeframe-control.playing::after {
top: 50%;
width: 0.35em;
height: 1.25em;
transform: translateY(-50%);
border: 0;
background: white;
}

.freezeframe-control.playing::before {
left: 36%;
}

.freezeframe-control.playing::after {
position: absolute;
left: 56%;
content: '';
}
</style>
3 changes: 2 additions & 1 deletion web/src/lib/components/content/Imeta.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
let url = $derived(
tag.find((entry) => entry.startsWith('url '))?.substring('url '.length) ?? ''
);
let mimeType = $derived(tag.find((entry) => entry.startsWith('m '))?.substring('m '.length));
</script>

{#if URL.canParse(url)}
<Img url={new URL(url)} />
<Img url={new URL(url)} {mimeType} />
{/if}
28 changes: 25 additions & 3 deletions web/src/lib/components/content/Img.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
<script lang="ts">
import { getAnimatedImageType } from '$lib/media/AnimatedImage';
import { createAnimatedImageState } from '$lib/media/AnimatedImageState.svelte';
import { followees } from '$lib/stores/Author';
import { imageOptimization } from '$lib/stores/Preference';
import { enableAutoPlayAnimatedImages, imageOptimization } from '$lib/stores/Preference';
import type { Event } from 'nostr-typedef';
import { getContext } from 'svelte';
import { _ } from 'svelte-i18n';
import { Img, type ImgSrc } from 'svelte-remote-image';
import FreezeframeImage from './FreezeframeImage.svelte';

interface Props {
url: URL;
mimeType?: string | null;
}

let { url }: Props = $props();
let { url, mimeType = undefined }: Props = $props();

const { href: src, pathname } = $derived(url);
const animatedImageType = $derived(getAnimatedImageType(mimeType, pathname));
const animatedImage = createAnimatedImageState({
src: () => src,
type: () => animatedImageType,
autoPlay: () => $enableAutoPlayAnimatedImages
});
const events = getContext<Event[] | undefined>('events');
const blur = events !== undefined && !events.some((event) => $followees.includes(event.pubkey));

Expand All @@ -23,7 +33,11 @@
</script>

<span class="img-wrapper">
{#if $imageOptimization && /\.(avif|jpg|jpeg|png|webp)$/i.test(pathname) && !src.startsWith($imageOptimization)}
{#if !$enableAutoPlayAnimatedImages && animatedImage.animated}
<FreezeframeImage {src} alt={src} {blur} />
{:else if !$enableAutoPlayAnimatedImages && animatedImage.checking}
<span class="global-content-image animated-image-checking"></span>
{:else if $imageOptimization && /\.(avif|jpg|jpeg|png|webp)$/i.test(pathname) && !src.startsWith($imageOptimization)}
<Img class="global-content-image{blur ? ' blur' : ''}" src={imageSrc} alt={src} />
{:else}
<img class="global-content-image" class:blur {src} alt={src} />
Expand All @@ -40,9 +54,17 @@
margin: 0.5em;
border: 1px solid lightgray;
border-radius: 5px;
box-sizing: border-box;
vertical-align: middle;
}

.img-wrapper :global(.global-content-image.animated-image-checking) {
display: inline-block;
width: min(16em, calc(100% - 1.5em));
height: 10em;
background: var(--accent-surface-low);
}

.img-wrapper :global(.global-content-image.blur),
img.blur {
filter: blur(8px);
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/components/content/Url.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
{#if preview}
<div>
<a href={link.href} target="_blank" rel="noopener noreferrer">
<Img url={link} />
<Img url={link} mimeType={contentType} />
</a>
</div>
{:else}
Expand Down
1 change: 1 addition & 0 deletions web/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
},
"auto_refresh": "Auto refresh",
"media_preview": "Media preview",
"autoplay_animated_images": "Auto-play animated images",
"image_optimization": "Image size reduction",
"image_optimization_none": "None",
"media_uploader": {
Expand Down
1 change: 1 addition & 0 deletions web/src/lib/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
},
"auto_refresh": "自動更新",
"media_preview": "メディアプレビュー",
"autoplay_animated_images": "アニメーション画像を自動再生",
"image_optimization": "画像軽量化",
"image_optimization_none": "なし",
"media_uploader": {
Expand Down
Loading
Loading