2024-08-08 23:32:46 +00:00
|
|
|
<script lang="ts">
|
2025-09-18 20:43:43 +00:00
|
|
|
import {randomId, call} from "@welshman/lib"
|
2025-09-18 19:17:31 +00:00
|
|
|
import {preventDefault, stopPropagation, compressFile} from "@lib/html"
|
2025-09-10 22:26:39 +00:00
|
|
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
|
|
|
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
|
|
|
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
2024-08-16 17:50:38 +00:00
|
|
|
import Icon from "@lib/components/Icon.svelte"
|
2026-07-02 21:29:30 +00:00
|
|
|
import Spinner from "@lib/components/Spinner.svelte"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {uploadFile} from "@app/uploads"
|
2024-08-08 23:32:46 +00:00
|
|
|
|
2025-02-04 00:37:14 +00:00
|
|
|
interface Props {
|
2025-02-04 04:43:18 +00:00
|
|
|
file?: File | undefined
|
|
|
|
|
url?: string | undefined
|
2025-02-04 00:37:14 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-04 04:43:18 +00:00
|
|
|
let {file = $bindable(), url = $bindable()}: Props = $props()
|
2024-08-08 23:32:46 +00:00
|
|
|
|
|
|
|
|
const id = randomId()
|
|
|
|
|
|
2026-07-02 21:39:21 +00:00
|
|
|
// Profile pictures are shown small (avatar-sized), so compress hard: this keeps
|
|
|
|
|
// the base64 fallback below small enough to embed directly in the profile event.
|
|
|
|
|
const compressOptions = {maxWidth: 128, maxHeight: 128, quality: 0.6, mimeType: "image/jpeg"}
|
|
|
|
|
|
2024-08-08 23:32:46 +00:00
|
|
|
const onDragEnter = () => {
|
|
|
|
|
active = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onDragOver = () => {
|
|
|
|
|
active = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onDragLeave = () => {
|
|
|
|
|
active = false
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 12:40:02 +00:00
|
|
|
const onDrop = async (e: any) => {
|
2024-08-08 23:32:46 +00:00
|
|
|
active = false
|
|
|
|
|
|
2026-07-02 21:39:21 +00:00
|
|
|
file = await compressFile(e.dataTransfer.files[0], compressOptions)
|
2024-08-08 23:32:46 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-15 12:40:02 +00:00
|
|
|
const onChange = async (e: any) => {
|
2026-07-02 21:39:21 +00:00
|
|
|
file = await compressFile(e.target.files[0], compressOptions)
|
2024-08-08 23:32:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onClear = () => {
|
2025-02-04 04:43:18 +00:00
|
|
|
initialUrl = undefined
|
|
|
|
|
file = undefined
|
|
|
|
|
url = undefined
|
2024-08-08 23:32:46 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-04 00:37:14 +00:00
|
|
|
let active = $state(false)
|
2026-07-02 21:29:30 +00:00
|
|
|
let loading = $state(false)
|
2025-02-04 00:37:14 +00:00
|
|
|
let initialUrl = $state(url)
|
2024-08-08 23:32:46 +00:00
|
|
|
|
2025-02-04 03:28:29 +00:00
|
|
|
$effect(() => {
|
2025-09-18 20:43:43 +00:00
|
|
|
call(async () => {
|
|
|
|
|
if (file) {
|
2026-07-02 21:29:30 +00:00
|
|
|
loading = true
|
|
|
|
|
|
2026-07-02 21:39:21 +00:00
|
|
|
const {result} = await uploadFile(file)
|
2026-07-02 21:29:30 +00:00
|
|
|
|
|
|
|
|
loading = false
|
2025-09-18 20:43:43 +00:00
|
|
|
|
|
|
|
|
if (result?.url) {
|
|
|
|
|
url = result.url
|
|
|
|
|
} else {
|
2026-07-02 21:39:21 +00:00
|
|
|
const reader = new FileReader()
|
|
|
|
|
|
|
|
|
|
reader.addEventListener(
|
|
|
|
|
"load",
|
|
|
|
|
() => {
|
|
|
|
|
url = reader.result as string
|
|
|
|
|
},
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
reader.readAsDataURL(file)
|
2025-09-18 20:43:43 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
url = initialUrl
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-02-04 00:37:14 +00:00
|
|
|
})
|
2024-08-08 23:32:46 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<form>
|
2026-07-02 21:39:21 +00:00
|
|
|
<input
|
|
|
|
|
{id}
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/jpeg,image/png,image/webp"
|
|
|
|
|
onchange={onChange}
|
|
|
|
|
class="hidden" />
|
2024-08-08 23:32:46 +00:00
|
|
|
<label
|
|
|
|
|
for={id}
|
|
|
|
|
aria-label="Drag and drop files here."
|
2026-06-24 20:56:53 +00:00
|
|
|
style="background-image: url({url}); border-color: {active ? 'var(--primary)' : 'var(--line)'};"
|
|
|
|
|
class="bg-surface-more relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-solid bg-cover bg-center transition-all"
|
2024-10-03 19:27:41 +00:00
|
|
|
class:transparent={!url}
|
2025-02-04 00:37:14 +00:00
|
|
|
ondragenter={stopPropagation(preventDefault(onDragEnter))}
|
|
|
|
|
ondragover={stopPropagation(preventDefault(onDragOver))}
|
|
|
|
|
ondragleave={stopPropagation(preventDefault(onDragLeave))}
|
|
|
|
|
ondrop={stopPropagation(preventDefault(onDrop))}>
|
2026-07-02 21:29:30 +00:00
|
|
|
<button
|
2026-07-02 21:39:21 +00:00
|
|
|
onclick={url ? stopPropagation(onClear) : undefined}
|
2026-07-02 21:29:30 +00:00
|
|
|
class="absolute right-0 top-0 h-5 w-5 overflow-hidden rounded-full flex items-center justify-center"
|
2026-06-24 20:56:53 +00:00
|
|
|
style="background: {url ? 'var(--error)' : 'var(--primary)'};">
|
2024-10-03 19:27:41 +00:00
|
|
|
{#if url}
|
2026-07-02 21:29:30 +00:00
|
|
|
<Icon icon={CloseCircle} class="scale-150 bg-surface-more!" />
|
2024-08-08 23:32:46 +00:00
|
|
|
{:else}
|
2026-06-24 20:56:53 +00:00
|
|
|
<Icon icon={AddCircle} class="scale-150 bg-surface-more!" />
|
2024-08-08 23:32:46 +00:00
|
|
|
{/if}
|
2026-07-02 21:29:30 +00:00
|
|
|
</button>
|
|
|
|
|
{#if loading}
|
|
|
|
|
<div class="absolute inset-0 flex items-center justify-center rounded-full bg-black/40">
|
|
|
|
|
<Spinner />
|
|
|
|
|
</div>
|
|
|
|
|
{:else if !url}
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={GallerySend} size={7} />
|
2024-08-08 23:32:46 +00:00
|
|
|
{/if}
|
|
|
|
|
</label>
|
|
|
|
|
</form>
|