atagen

malice

public
Star0
Watch0
294426c7f88fd4348a61b8a8590233237a1e52c0
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/StickerPicker.svelte

d095131eec0eatagen2026-10-24 08:39 UTC1
<script lang="ts">
d095131eec0eatagen2026-10-24 08:39 UTC2
  import { onMount } from 'svelte';
d095131eec0eatagen2026-10-24 08:39 UTC3
  import { messageMediaUrl } from './api';
d095131eec0eatagen2026-10-24 08:39 UTC4
  import MxcAvatar from './MxcAvatar.svelte';
445bf199a9cfatagen2026-10-25 02:56 UTC5
  import EmojiText from './EmojiText.svelte';
d095131eec0eatagen2026-10-24 08:39 UTC6
  import { packLabel, stickersOf } from './packs';
d095131eec0eatagen2026-10-24 08:39 UTC7
  import type { ImagePack, PackImage } from './types';
0e50e0d137afatagen2026-10-26 05:50 UTC8
  import { longpress } from './longpress';
d095131eec0eatagen2026-10-24 08:39 UTC9
d095131eec0eatagen2026-10-24 08:39 UTC10
  export let packs: ImagePack[];
d095131eec0eatagen2026-10-24 08:39 UTC11
  export let roomName: (roomId: string) => string;
d095131eec0eatagen2026-10-24 08:39 UTC12
  export let onSend: (pack: ImagePack, image: PackImage) => void;
d095131eec0eatagen2026-10-24 08:39 UTC13
  export let onClone: (pack: ImagePack, image: PackImage, x: number, y: number) => void;
d095131eec0eatagen2026-10-24 08:39 UTC14
  export let onClonePack: (pack: ImagePack, x: number, y: number) => void;
d095131eec0eatagen2026-10-24 08:39 UTC15
  export let onManage: () => void;
d095131eec0eatagen2026-10-24 08:39 UTC16
  export let onClose: () => void;
d095131eec0eatagen2026-10-24 08:39 UTC17
d095131eec0eatagen2026-10-24 08:39 UTC18
  let root: HTMLDivElement;
d095131eec0eatagen2026-10-24 08:39 UTC19
  let search: HTMLInputElement;
d095131eec0eatagen2026-10-24 08:39 UTC20
  let query = '';
d095131eec0eatagen2026-10-24 08:39 UTC21
  let activeId: string | null = null;
d095131eec0eatagen2026-10-24 08:39 UTC22
d095131eec0eatagen2026-10-24 08:39 UTC23
  $: withStickers = packs.filter((p) => stickersOf(p).length > 0);
d095131eec0eatagen2026-10-24 08:39 UTC24
  $: q = query.trim().toLowerCase();
d095131eec0eatagen2026-10-24 08:39 UTC25
  $: shown = withStickers
d095131eec0eatagen2026-10-24 08:39 UTC26
    .map((pack) => ({
d095131eec0eatagen2026-10-24 08:39 UTC27
      pack,
d095131eec0eatagen2026-10-24 08:39 UTC28
      images: stickersOf(pack).filter(
d095131eec0eatagen2026-10-24 08:39 UTC29
        (i) => !q || i.shortcode.toLowerCase().includes(q) || (i.body ?? '').toLowerCase().includes(q)
d095131eec0eatagen2026-10-24 08:39 UTC30
      )
d095131eec0eatagen2026-10-24 08:39 UTC31
    }))
d095131eec0eatagen2026-10-24 08:39 UTC32
    .filter((g) => g.images.length > 0 && (q || activeId === null || g.pack.id === activeId));
d095131eec0eatagen2026-10-24 08:39 UTC33
d095131eec0eatagen2026-10-24 08:39 UTC34
  onMount(() => search?.focus());
d095131eec0eatagen2026-10-24 08:39 UTC35
d095131eec0eatagen2026-10-24 08:39 UTC36
  function outside(ev: PointerEvent): void {
d095131eec0eatagen2026-10-24 08:39 UTC37
    if (!root.contains(ev.target as Node) && !(ev.target as Element).closest?.('.context-menu, .sticker-btn')) onClose();
d095131eec0eatagen2026-10-24 08:39 UTC38
  }
d095131eec0eatagen2026-10-24 08:39 UTC39
d095131eec0eatagen2026-10-24 08:39 UTC40
  function key(ev: KeyboardEvent): void {
d095131eec0eatagen2026-10-24 08:39 UTC41
    if (ev.key === 'Escape' && !document.querySelector('.context-menu')) {
d095131eec0eatagen2026-10-24 08:39 UTC42
      ev.stopPropagation();
d095131eec0eatagen2026-10-24 08:39 UTC43
      onClose();
d095131eec0eatagen2026-10-24 08:39 UTC44
    }
d095131eec0eatagen2026-10-24 08:39 UTC45
  }
d095131eec0eatagen2026-10-24 08:39 UTC46
</script>
d095131eec0eatagen2026-10-24 08:39 UTC47
d095131eec0eatagen2026-10-24 08:39 UTC48
<svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} />
d095131eec0eatagen2026-10-24 08:39 UTC49
d095131eec0eatagen2026-10-24 08:39 UTC50
<div class="sticker-picker" role="dialog" aria-label="Stickers" bind:this={root}>
d095131eec0eatagen2026-10-24 08:39 UTC51
  <div class="top">
d095131eec0eatagen2026-10-24 08:39 UTC52
    <input class="search" type="search" placeholder="Search stickers" bind:value={query} bind:this={search} />
d095131eec0eatagen2026-10-24 08:39 UTC53
    <button type="button" class="icon-btn" title="Manage packs" aria-label="Manage packs" on:click={onManage}>
05a48e9e7942atagen2026-10-24 09:32 UTC54
      <svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1L7 17M17 7l2.1-2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
d095131eec0eatagen2026-10-24 08:39 UTC55
    </button>
d095131eec0eatagen2026-10-24 08:39 UTC56
  </div>
d095131eec0eatagen2026-10-24 08:39 UTC57
  {#if withStickers.length > 1}
d095131eec0eatagen2026-10-24 08:39 UTC58
    <div class="tabs" role="tablist" aria-label="Packs">
d095131eec0eatagen2026-10-24 08:39 UTC59
      <button type="button" role="tab" class="tab all" aria-selected={activeId === null} on:click={() => (activeId = null)}>All</button>
d095131eec0eatagen2026-10-24 08:39 UTC60
      {#each withStickers as pack (pack.id)}
d095131eec0eatagen2026-10-24 08:39 UTC61
        <button
d095131eec0eatagen2026-10-24 08:39 UTC62
          type="button"
d095131eec0eatagen2026-10-24 08:39 UTC63
          role="tab"
d095131eec0eatagen2026-10-24 08:39 UTC64
          class="tab"
d095131eec0eatagen2026-10-24 08:39 UTC65
          aria-selected={activeId === pack.id}
d095131eec0eatagen2026-10-24 08:39 UTC66
          title={packLabel(pack, roomName)}
d095131eec0eatagen2026-10-24 08:39 UTC67
          on:click={() => (activeId = pack.id)}
d095131eec0eatagen2026-10-24 08:39 UTC68
        >
d095131eec0eatagen2026-10-24 08:39 UTC69
          {#if pack.avatar}
d095131eec0eatagen2026-10-24 08:39 UTC70
            <MxcAvatar mxc={pack.avatar} label={packLabel(pack, roomName)} size={24} shape="rounded" />
d095131eec0eatagen2026-10-24 08:39 UTC71
          {:else}
d095131eec0eatagen2026-10-24 08:39 UTC72
            <img src={messageMediaUrl(stickersOf(pack)[0].media_id, 'Thumbnail')} alt={packLabel(pack, roomName)} />
d095131eec0eatagen2026-10-24 08:39 UTC73
          {/if}
d095131eec0eatagen2026-10-24 08:39 UTC74
        </button>
d095131eec0eatagen2026-10-24 08:39 UTC75
      {/each}
d095131eec0eatagen2026-10-24 08:39 UTC76
    </div>
d095131eec0eatagen2026-10-24 08:39 UTC77
  {/if}
d095131eec0eatagen2026-10-24 08:39 UTC78
  <div class="scroll">
d095131eec0eatagen2026-10-24 08:39 UTC79
    {#each shown as g (g.pack.id)}
d095131eec0eatagen2026-10-24 08:39 UTC80
      <section class="group" aria-label={packLabel(g.pack, roomName)}>
d095131eec0eatagen2026-10-24 08:39 UTC81
        <div class="group-head">
445bf199a9cfatagen2026-10-25 02:56 UTC82
          <h3><EmojiText text={packLabel(g.pack, roomName)} /></h3>
132eeceeb362atagen2026-10-25 03:34 UTC83
          <button type="button" class="clone-pack" on:click={(ev) => onClonePack(g.pack, ev.clientX, ev.clientY)}>Clone pack</button>
d095131eec0eatagen2026-10-24 08:39 UTC84
        </div>
d095131eec0eatagen2026-10-24 08:39 UTC85
        <div class="grid">
d095131eec0eatagen2026-10-24 08:39 UTC86
          {#each g.images as image (image.shortcode)}
d095131eec0eatagen2026-10-24 08:39 UTC87
            <button
d095131eec0eatagen2026-10-24 08:39 UTC88
              type="button"
d095131eec0eatagen2026-10-24 08:39 UTC89
              class="cell"
d095131eec0eatagen2026-10-24 08:39 UTC90
              title={image.body ?? image.shortcode}
d095131eec0eatagen2026-10-24 08:39 UTC91
              aria-label={image.body ?? image.shortcode}
d095131eec0eatagen2026-10-24 08:39 UTC92
              on:click={() => onSend(g.pack, image)}
d095131eec0eatagen2026-10-24 08:39 UTC93
              on:contextmenu|preventDefault={(ev) => onClone(g.pack, image, ev.clientX, ev.clientY)}
0e50e0d137afatagen2026-10-26 05:50 UTC94
              use:longpress={(ev) => onClone(g.pack, image, ev.clientX, ev.clientY)}
d095131eec0eatagen2026-10-24 08:39 UTC95
            >
d095131eec0eatagen2026-10-24 08:39 UTC96
              <img src={messageMediaUrl(image.media_id, 'Thumbnail')} alt="" loading="lazy" />
d095131eec0eatagen2026-10-24 08:39 UTC97
            </button>
d095131eec0eatagen2026-10-24 08:39 UTC98
          {/each}
d095131eec0eatagen2026-10-24 08:39 UTC99
        </div>
d095131eec0eatagen2026-10-24 08:39 UTC100
      </section>
d095131eec0eatagen2026-10-24 08:39 UTC101
    {:else}
05a48e9e7942atagen2026-10-24 09:32 UTC102
      {#if withStickers.length === 0}
05a48e9e7942atagen2026-10-24 09:32 UTC103
        <div class="empty">
05a48e9e7942atagen2026-10-24 09:32 UTC104
          <p class="status">No stickers yet</p>
05a48e9e7942atagen2026-10-24 09:32 UTC105
          <button type="button" class="primary-btn" on:click={onManage}>Add a pack</button>
05a48e9e7942atagen2026-10-24 09:32 UTC106
        </div>
05a48e9e7942atagen2026-10-24 09:32 UTC107
      {:else}
445bf199a9cfatagen2026-10-25 02:56 UTC108
        <p class="status">No stickers match "<EmojiText text={query.trim()} />"</p>
05a48e9e7942atagen2026-10-24 09:32 UTC109
      {/if}
d095131eec0eatagen2026-10-24 08:39 UTC110
    {/each}
d095131eec0eatagen2026-10-24 08:39 UTC111
  </div>
d095131eec0eatagen2026-10-24 08:39 UTC112
</div>
d095131eec0eatagen2026-10-24 08:39 UTC113
d095131eec0eatagen2026-10-24 08:39 UTC114
<style>
d095131eec0eatagen2026-10-24 08:39 UTC115
  .sticker-picker {
d095131eec0eatagen2026-10-24 08:39 UTC116
    display: flex;
d095131eec0eatagen2026-10-24 08:39 UTC117
    flex-direction: column;
d095131eec0eatagen2026-10-24 08:39 UTC118
    gap: 6px;
d095131eec0eatagen2026-10-24 08:39 UTC119
    width: 360px;
05a48e9e7942atagen2026-10-24 09:32 UTC120
    height: auto;
0e50e0d137afatagen2026-10-26 05:50 UTC121
    max-height: min(420px, calc(100dvh - 120px));
d095131eec0eatagen2026-10-24 08:39 UTC122
    max-width: calc(100vw - 16px);
d095131eec0eatagen2026-10-24 08:39 UTC123
    padding: 8px;
d095131eec0eatagen2026-10-24 08:39 UTC124
    background: var(--panel);
d095131eec0eatagen2026-10-24 08:39 UTC125
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC126
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC127
    box-shadow: 0 6px 24px rgb(0 0 0 / 0.5);
d095131eec0eatagen2026-10-24 08:39 UTC128
  }
d095131eec0eatagen2026-10-24 08:39 UTC129
  .top { display: flex; gap: 6px; }
d095131eec0eatagen2026-10-24 08:39 UTC130
  .search {
d095131eec0eatagen2026-10-24 08:39 UTC131
    flex: 1;
d095131eec0eatagen2026-10-24 08:39 UTC132
    background: var(--bg-soft);
d095131eec0eatagen2026-10-24 08:39 UTC133
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC134
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC135
    color: var(--text);
d095131eec0eatagen2026-10-24 08:39 UTC136
    padding: 6px 10px;
d095131eec0eatagen2026-10-24 08:39 UTC137
    font-size: 13px;
d095131eec0eatagen2026-10-24 08:39 UTC138
  }
d095131eec0eatagen2026-10-24 08:39 UTC139
  .search:focus-visible { outline: none; border-color: var(--accent); }
d095131eec0eatagen2026-10-24 08:39 UTC140
  .tabs { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 2px; }
d095131eec0eatagen2026-10-24 08:39 UTC141
  .tab {
d095131eec0eatagen2026-10-24 08:39 UTC142
    flex: none;
d095131eec0eatagen2026-10-24 08:39 UTC143
    display: grid;
d095131eec0eatagen2026-10-24 08:39 UTC144
    place-items: center;
d095131eec0eatagen2026-10-24 08:39 UTC145
    min-width: 32px;
d095131eec0eatagen2026-10-24 08:39 UTC146
    height: 32px;
d095131eec0eatagen2026-10-24 08:39 UTC147
    padding: 2px 6px;
d095131eec0eatagen2026-10-24 08:39 UTC148
    border: 1px solid transparent;
05a48e9e7942atagen2026-10-24 09:32 UTC149
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC150
    background: transparent;
d095131eec0eatagen2026-10-24 08:39 UTC151
    color: var(--muted);
d095131eec0eatagen2026-10-24 08:39 UTC152
    font-size: 12px;
d095131eec0eatagen2026-10-24 08:39 UTC153
    cursor: pointer;
d095131eec0eatagen2026-10-24 08:39 UTC154
  }
d095131eec0eatagen2026-10-24 08:39 UTC155
  .tab img { width: 24px; height: 24px; object-fit: contain; }
d095131eec0eatagen2026-10-24 08:39 UTC156
  .tab[aria-selected='true'] { border-color: var(--accent); color: var(--text); }
d095131eec0eatagen2026-10-24 08:39 UTC157
  .scroll { flex: 1; overflow-y: auto; min-height: 0; }
d095131eec0eatagen2026-10-24 08:39 UTC158
  .group-head { display: flex; align-items: center; justify-content: space-between; margin: 6px 2px 4px; }
d095131eec0eatagen2026-10-24 08:39 UTC159
  h3 { margin: 0; font-size: 12px; font-weight: 650; color: var(--muted); }
d095131eec0eatagen2026-10-24 08:39 UTC160
  .clone-pack {
d095131eec0eatagen2026-10-24 08:39 UTC161
    border: none;
d095131eec0eatagen2026-10-24 08:39 UTC162
    background: none;
d095131eec0eatagen2026-10-24 08:39 UTC163
    color: var(--accent);
d095131eec0eatagen2026-10-24 08:39 UTC164
    font-size: 12px;
d095131eec0eatagen2026-10-24 08:39 UTC165
    cursor: pointer;
d095131eec0eatagen2026-10-24 08:39 UTC166
  }
d095131eec0eatagen2026-10-24 08:39 UTC167
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(76px, 1fr)); gap: 4px; }
d095131eec0eatagen2026-10-24 08:39 UTC168
  .cell {
d095131eec0eatagen2026-10-24 08:39 UTC169
    aspect-ratio: 1;
d095131eec0eatagen2026-10-24 08:39 UTC170
    padding: 4px;
d095131eec0eatagen2026-10-24 08:39 UTC171
    border: 1px solid transparent;
05a48e9e7942atagen2026-10-24 09:32 UTC172
    border-radius: var(--r-md);
d095131eec0eatagen2026-10-24 08:39 UTC173
    background: transparent;
d095131eec0eatagen2026-10-24 08:39 UTC174
    cursor: pointer;
d095131eec0eatagen2026-10-24 08:39 UTC175
  }
d095131eec0eatagen2026-10-24 08:39 UTC176
  .cell:hover, .cell:focus-visible { background: var(--hover); border-color: var(--border); outline: none; }
d095131eec0eatagen2026-10-24 08:39 UTC177
  .cell img { width: 100%; height: 100%; object-fit: contain; }
05a48e9e7942atagen2026-10-24 09:32 UTC178
  .status { color: var(--muted); font-size: 13px; text-align: center; margin: 16px 0 10px; }
05a48e9e7942atagen2026-10-24 09:32 UTC179
  .empty { display: flex; flex-direction: column; align-items: center; padding-bottom: 12px; }
d095131eec0eatagen2026-10-24 08:39 UTC180
</style>