atagen

malice

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

Blame: web/src/lib/PackCloneDialog.svelte

132eeceeb362atagen2026-10-25 03:34 UTC1
<script lang="ts">
132eeceeb362atagen2026-10-25 03:34 UTC2
  import { runCommand } from './api';
132eeceeb362atagen2026-10-25 03:34 UTC3
  import type { CloneSource } from './pack-clone';
132eeceeb362atagen2026-10-25 03:34 UTC4
  import { cloneRequest, editableChoices, freePackStateKey, packLabel } from './packs';
132eeceeb362atagen2026-10-25 03:34 UTC5
  import type { PackTarget } from './transport';
132eeceeb362atagen2026-10-25 03:34 UTC6
  import type { ImagePacks, PackUsage, RoomSummary } from './types';
132eeceeb362atagen2026-10-25 03:34 UTC7
132eeceeb362atagen2026-10-25 03:34 UTC8
  export let source: CloneSource;
132eeceeb362atagen2026-10-25 03:34 UTC9
  export let packs: ImagePacks;
132eeceeb362atagen2026-10-25 03:34 UTC10
  export let rooms: RoomSummary[];
132eeceeb362atagen2026-10-25 03:34 UTC11
  export let onClose: () => void;
132eeceeb362atagen2026-10-25 03:34 UTC12
  export let onError: (message: string) => void;
132eeceeb362atagen2026-10-25 03:34 UTC13
132eeceeb362atagen2026-10-25 03:34 UTC14
  const roomName = (id: string) => rooms.find((room) => room.room_id === id)?.name ?? id;
132eeceeb362atagen2026-10-25 03:34 UTC15
  let destinationId = '';
132eeceeb362atagen2026-10-25 03:34 UTC16
  let roomId = '';
132eeceeb362atagen2026-10-25 03:34 UTC17
  let name = source.kind === 'sticker' ? 'Sticker pack' : packLabel(source.pack, roomName);
132eeceeb362atagen2026-10-25 03:34 UTC18
  let asSticker = true;
132eeceeb362atagen2026-10-25 03:34 UTC19
  let asEmoji = true;
132eeceeb362atagen2026-10-25 03:34 UTC20
  let busy = false;
132eeceeb362atagen2026-10-25 03:34 UTC21
  let error = '';
132eeceeb362atagen2026-10-25 03:34 UTC22
  let createdTarget: { roomId: string; name: string; target: PackTarget } | null = null;
132eeceeb362atagen2026-10-25 03:34 UTC23
132eeceeb362atagen2026-10-25 03:34 UTC24
  $: choices = editableChoices(packs, roomName).filter((choice) =>
132eeceeb362atagen2026-10-25 03:34 UTC25
    source.kind !== 'pack' || choice.pack?.id !== source.pack.id);
132eeceeb362atagen2026-10-25 03:34 UTC26
  $: locations = rooms.filter((room) => room.can_edit_packs && room.membership === 'Joined')
132eeceeb362atagen2026-10-25 03:34 UTC27
    .sort((a, b) => Number(b.space) - Number(a.space) || a.name.localeCompare(b.name));
132eeceeb362atagen2026-10-25 03:34 UTC28
  $: if (!destinationId) destinationId = choices[0]?.pack?.id ?? (choices.length ? 'user' : 'new');
132eeceeb362atagen2026-10-25 03:34 UTC29
  $: if (!roomId && locations.length) roomId = locations[0].room_id;
132eeceeb362atagen2026-10-25 03:34 UTC30
  $: creating = destinationId === 'new';
132eeceeb362atagen2026-10-25 03:34 UTC31
132eeceeb362atagen2026-10-25 03:34 UTC32
  function key(event: KeyboardEvent): void {
132eeceeb362atagen2026-10-25 03:34 UTC33
    if (event.key !== 'Escape') return;
132eeceeb362atagen2026-10-25 03:34 UTC34
    event.stopImmediatePropagation();
132eeceeb362atagen2026-10-25 03:34 UTC35
    if (!busy) onClose();
132eeceeb362atagen2026-10-25 03:34 UTC36
  }
132eeceeb362atagen2026-10-25 03:34 UTC37
132eeceeb362atagen2026-10-25 03:34 UTC38
  async function copy(): Promise<void> {
132eeceeb362atagen2026-10-25 03:34 UTC39
    if (busy) return;
132eeceeb362atagen2026-10-25 03:34 UTC40
    const usage: PackUsage[] = [...(asSticker ? ['Sticker' as const] : []), ...(asEmoji ? ['Emoticon' as const] : [])];
132eeceeb362atagen2026-10-25 03:34 UTC41
    if (!usage.length) {
132eeceeb362atagen2026-10-25 03:34 UTC42
      error = 'Choose stickers, emoji, or both';
132eeceeb362atagen2026-10-25 03:34 UTC43
      return;
132eeceeb362atagen2026-10-25 03:34 UTC44
    }
132eeceeb362atagen2026-10-25 03:34 UTC45
    busy = true;
132eeceeb362atagen2026-10-25 03:34 UTC46
    error = '';
132eeceeb362atagen2026-10-25 03:34 UTC47
    try {
132eeceeb362atagen2026-10-25 03:34 UTC48
      const choice = choices.find((item) => (item.pack?.id ?? 'user') === destinationId);
132eeceeb362atagen2026-10-25 03:34 UTC49
      let target = choice?.target;
132eeceeb362atagen2026-10-25 03:34 UTC50
      const displayName = name.trim();
132eeceeb362atagen2026-10-25 03:34 UTC51
      if (creating) {
132eeceeb362atagen2026-10-25 03:34 UTC52
        if (!roomId || !displayName) throw new Error('Choose a space or room and give the pack a name');
132eeceeb362atagen2026-10-25 03:34 UTC53
        if (!createdTarget || createdTarget.roomId !== roomId || createdTarget.name !== displayName) {
132eeceeb362atagen2026-10-25 03:34 UTC54
          const stateKey = freePackStateKey(packs, roomId, displayName);
132eeceeb362atagen2026-10-25 03:34 UTC55
          target = { kind: 'room', room_id: roomId, state_key: stateKey };
132eeceeb362atagen2026-10-25 03:34 UTC56
          await runCommand({ op: 'create_room_pack', room_id: roomId, state_key: stateKey, display_name: displayName });
132eeceeb362atagen2026-10-25 03:34 UTC57
          createdTarget = { roomId, name: displayName, target };
132eeceeb362atagen2026-10-25 03:34 UTC58
        } else target = createdTarget.target;
132eeceeb362atagen2026-10-25 03:34 UTC59
      }
132eeceeb362atagen2026-10-25 03:34 UTC60
      if (!target) throw new Error('Choose a destination pack');
132eeceeb362atagen2026-10-25 03:34 UTC61
      const result = await runCommand(cloneRequest(source, target, usage, creating ? displayName : null, choice?.pack ?? null)) as { skipped?: string[] };
132eeceeb362atagen2026-10-25 03:34 UTC62
      if (result?.skipped?.length) onError(`Could not copy ${result.skipped.join(', ')}`);
132eeceeb362atagen2026-10-25 03:34 UTC63
      onClose();
132eeceeb362atagen2026-10-25 03:34 UTC64
    } catch (e) {
132eeceeb362atagen2026-10-25 03:34 UTC65
      error = e instanceof Error ? e.message : 'Could not copy the images';
132eeceeb362atagen2026-10-25 03:34 UTC66
    } finally {
132eeceeb362atagen2026-10-25 03:34 UTC67
      busy = false;
132eeceeb362atagen2026-10-25 03:34 UTC68
    }
132eeceeb362atagen2026-10-25 03:34 UTC69
  }
132eeceeb362atagen2026-10-25 03:34 UTC70
</script>
132eeceeb362atagen2026-10-25 03:34 UTC71
132eeceeb362atagen2026-10-25 03:34 UTC72
<svelte:window on:keydown|capture={key} />
132eeceeb362atagen2026-10-25 03:34 UTC73
132eeceeb362atagen2026-10-25 03:34 UTC74
<div class="modal clone" role="dialog" aria-modal="true" aria-label={source.kind === 'pack' ? 'Clone pack' : 'Clone image'}>
132eeceeb362atagen2026-10-25 03:34 UTC75
  <h2>{source.kind === 'pack' ? 'Clone pack' : 'Clone image'}</h2>
132eeceeb362atagen2026-10-25 03:34 UTC76
  <form on:submit|preventDefault={() => void copy()}>
132eeceeb362atagen2026-10-25 03:34 UTC77
    <fieldset disabled={busy}>
132eeceeb362atagen2026-10-25 03:34 UTC78
      <label class="field">
132eeceeb362atagen2026-10-25 03:34 UTC79
        <span>Destination pack</span>
132eeceeb362atagen2026-10-25 03:34 UTC80
        <select bind:value={destinationId}>
132eeceeb362atagen2026-10-25 03:34 UTC81
          {#each choices as choice}
132eeceeb362atagen2026-10-25 03:34 UTC82
            <option value={choice.pack?.id ?? 'user'}>{choice.label}</option>
132eeceeb362atagen2026-10-25 03:34 UTC83
          {/each}
132eeceeb362atagen2026-10-25 03:34 UTC84
          {#if locations.length}<option value="new">New pack in a space or room</option>{/if}
132eeceeb362atagen2026-10-25 03:34 UTC85
        </select>
132eeceeb362atagen2026-10-25 03:34 UTC86
      </label>
132eeceeb362atagen2026-10-25 03:34 UTC87
      {#if creating && locations.length}
132eeceeb362atagen2026-10-25 03:34 UTC88
        <label class="field">
132eeceeb362atagen2026-10-25 03:34 UTC89
          <span>Space or room</span>
132eeceeb362atagen2026-10-25 03:34 UTC90
          <select bind:value={roomId}>
132eeceeb362atagen2026-10-25 03:34 UTC91
            {#each locations as room (room.room_id)}
132eeceeb362atagen2026-10-25 03:34 UTC92
              <option value={room.room_id}>{room.space ? 'Space' : 'Room'} · {room.name}</option>
132eeceeb362atagen2026-10-25 03:34 UTC93
            {/each}
132eeceeb362atagen2026-10-25 03:34 UTC94
          </select>
132eeceeb362atagen2026-10-25 03:34 UTC95
        </label>
132eeceeb362atagen2026-10-25 03:34 UTC96
        <label class="field"><span>New pack name</span><input type="text" bind:value={name} required /></label>
132eeceeb362atagen2026-10-25 03:34 UTC97
      {:else if choices.length === 0}
132eeceeb362atagen2026-10-25 03:34 UTC98
        <p>No writable destination packs are available. Creating shared packs requires permission in a joined space or room.</p>
132eeceeb362atagen2026-10-25 03:34 UTC99
      {:else}
132eeceeb362atagen2026-10-25 03:34 UTC100
        <p>Copies are appended to the selected pack. Existing images stay in place and duplicate names get a numbered suffix.</p>
132eeceeb362atagen2026-10-25 03:34 UTC101
      {/if}
132eeceeb362atagen2026-10-25 03:34 UTC102
      <div class="uses" role="group" aria-label="Use copied images as">
132eeceeb362atagen2026-10-25 03:34 UTC103
        <label><input type="checkbox" bind:checked={asSticker} /> Stickers</label>
132eeceeb362atagen2026-10-25 03:34 UTC104
        <label><input type="checkbox" bind:checked={asEmoji} /> Emoji and reactions</label>
132eeceeb362atagen2026-10-25 03:34 UTC105
      </div>
132eeceeb362atagen2026-10-25 03:34 UTC106
      {#if error}<p class="error" role="alert">{error}</p>{/if}
132eeceeb362atagen2026-10-25 03:34 UTC107
      <div class="actions">
132eeceeb362atagen2026-10-25 03:34 UTC108
        <button type="button" class="ghost-btn" on:click={onClose}>Cancel</button>
132eeceeb362atagen2026-10-25 03:34 UTC109
        <button type="submit" class="primary-btn" disabled={(!choices.length && !locations.length) || (!asSticker && !asEmoji)}>{busy ? 'Copying…' : 'Copy'}</button>
132eeceeb362atagen2026-10-25 03:34 UTC110
      </div>
132eeceeb362atagen2026-10-25 03:34 UTC111
    </fieldset>
132eeceeb362atagen2026-10-25 03:34 UTC112
  </form>
132eeceeb362atagen2026-10-25 03:34 UTC113
</div>
132eeceeb362atagen2026-10-25 03:34 UTC114
132eeceeb362atagen2026-10-25 03:34 UTC115
<style>
132eeceeb362atagen2026-10-25 03:34 UTC116
  .clone { width: min(460px, 100%); }
132eeceeb362atagen2026-10-25 03:34 UTC117
  h2 { margin: 0 0 16px; font-size: 18px; }
132eeceeb362atagen2026-10-25 03:34 UTC118
  fieldset { display: flex; flex-direction: column; gap: 14px; border: 0; padding: 0; margin: 0; min-width: 0; }
132eeceeb362atagen2026-10-25 03:34 UTC119
  .field { display: flex; flex-direction: column; gap: 6px; }
132eeceeb362atagen2026-10-25 03:34 UTC120
  input[type='text'], select { width: 100%; padding: 8px; background: var(--bg-soft); border: 1px solid var(--border); border-radius: var(--r-sm); color: var(--text); }
132eeceeb362atagen2026-10-25 03:34 UTC121
  p { margin: 0; font-size: 13px; color: var(--muted); }
132eeceeb362atagen2026-10-25 03:34 UTC122
  .uses, .actions { display: flex; flex-wrap: wrap; gap: 14px; }
132eeceeb362atagen2026-10-25 03:34 UTC123
  .uses label { display: flex; align-items: center; gap: 6px; font-size: 13px; }
132eeceeb362atagen2026-10-25 03:34 UTC124
  .actions { justify-content: flex-end; }
132eeceeb362atagen2026-10-25 03:34 UTC125
  .error { color: var(--danger); }
132eeceeb362atagen2026-10-25 03:34 UTC126
</style>