atagen

malice

public
Star0
Watch0
294426c7f88fd4348a61b8a8590233237a1e52c0
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/PackCloneDialog.svelteSvelte126 lines5.9 KB
1
<script lang="ts">
2
  import { runCommand } from './api';
3
  import type { CloneSource } from './pack-clone';
4
  import { cloneRequest, editableChoices, freePackStateKey, packLabel } from './packs';
5
  import type { PackTarget } from './transport';
6
  import type { ImagePacks, PackUsage, RoomSummary } from './types';
7
8
  export let source: CloneSource;
9
  export let packs: ImagePacks;
10
  export let rooms: RoomSummary[];
11
  export let onClose: () => void;
12
  export let onError: (message: string) => void;
13
14
  const roomName = (id: string) => rooms.find((room) => room.room_id === id)?.name ?? id;
15
  let destinationId = '';
16
  let roomId = '';
17
  let name = source.kind === 'sticker' ? 'Sticker pack' : packLabel(source.pack, roomName);
18
  let asSticker = true;
19
  let asEmoji = true;
20
  let busy = false;
21
  let error = '';
22
  let createdTarget: { roomId: string; name: string; target: PackTarget } | null = null;
23
24
  $: choices = editableChoices(packs, roomName).filter((choice) =>
25
    source.kind !== 'pack' || choice.pack?.id !== source.pack.id);
26
  $: locations = rooms.filter((room) => room.can_edit_packs && room.membership === 'Joined')
27
    .sort((a, b) => Number(b.space) - Number(a.space) || a.name.localeCompare(b.name));
28
  $: if (!destinationId) destinationId = choices[0]?.pack?.id ?? (choices.length ? 'user' : 'new');
29
  $: if (!roomId && locations.length) roomId = locations[0].room_id;
30
  $: creating = destinationId === 'new';
31
32
  function key(event: KeyboardEvent): void {
33
    if (event.key !== 'Escape') return;
34
    event.stopImmediatePropagation();
35
    if (!busy) onClose();
36
  }
37
38
  async function copy(): Promise<void> {
39
    if (busy) return;
40
    const usage: PackUsage[] = [...(asSticker ? ['Sticker' as const] : []), ...(asEmoji ? ['Emoticon' as const] : [])];
41
    if (!usage.length) {
42
      error = 'Choose stickers, emoji, or both';
43
      return;
44
    }
45
    busy = true;
46
    error = '';
47
    try {
48
      const choice = choices.find((item) => (item.pack?.id ?? 'user') === destinationId);
49
      let target = choice?.target;
50
      const displayName = name.trim();
51
      if (creating) {
52
        if (!roomId || !displayName) throw new Error('Choose a space or room and give the pack a name');
53
        if (!createdTarget || createdTarget.roomId !== roomId || createdTarget.name !== displayName) {
54
          const stateKey = freePackStateKey(packs, roomId, displayName);
55
          target = { kind: 'room', room_id: roomId, state_key: stateKey };
56
          await runCommand({ op: 'create_room_pack', room_id: roomId, state_key: stateKey, display_name: displayName });
57
          createdTarget = { roomId, name: displayName, target };
58
        } else target = createdTarget.target;
59
      }
60
      if (!target) throw new Error('Choose a destination pack');
61
      const result = await runCommand(cloneRequest(source, target, usage, creating ? displayName : null, choice?.pack ?? null)) as { skipped?: string[] };
62
      if (result?.skipped?.length) onError(`Could not copy ${result.skipped.join(', ')}`);
63
      onClose();
64
    } catch (e) {
65
      error = e instanceof Error ? e.message : 'Could not copy the images';
66
    } finally {
67
      busy = false;
68
    }
69
  }
70
</script>
71
72
<svelte:window on:keydown|capture={key} />
73
74
<div class="modal clone" role="dialog" aria-modal="true" aria-label={source.kind === 'pack' ? 'Clone pack' : 'Clone image'}>
75
  <h2>{source.kind === 'pack' ? 'Clone pack' : 'Clone image'}</h2>
76
  <form on:submit|preventDefault={() => void copy()}>
77
    <fieldset disabled={busy}>
78
      <label class="field">
79
        <span>Destination pack</span>
80
        <select bind:value={destinationId}>
81
          {#each choices as choice}
82
            <option value={choice.pack?.id ?? 'user'}>{choice.label}</option>
83
          {/each}
84
          {#if locations.length}<option value="new">New pack in a space or room</option>{/if}
85
        </select>
86
      </label>
87
      {#if creating && locations.length}
88
        <label class="field">
89
          <span>Space or room</span>
90
          <select bind:value={roomId}>
91
            {#each locations as room (room.room_id)}
92
              <option value={room.room_id}>{room.space ? 'Space' : 'Room'} · {room.name}</option>
93
            {/each}
94
          </select>
95
        </label>
96
        <label class="field"><span>New pack name</span><input type="text" bind:value={name} required /></label>
97
      {:else if choices.length === 0}
98
        <p>No writable destination packs are available. Creating shared packs requires permission in a joined space or room.</p>
99
      {:else}
100
        <p>Copies are appended to the selected pack. Existing images stay in place and duplicate names get a numbered suffix.</p>
101
      {/if}
102
      <div class="uses" role="group" aria-label="Use copied images as">
103
        <label><input type="checkbox" bind:checked={asSticker} /> Stickers</label>
104
        <label><input type="checkbox" bind:checked={asEmoji} /> Emoji and reactions</label>
105
      </div>
106
      {#if error}<p class="error" role="alert">{error}</p>{/if}
107
      <div class="actions">
108
        <button type="button" class="ghost-btn" on:click={onClose}>Cancel</button>
109
        <button type="submit" class="primary-btn" disabled={(!choices.length && !locations.length) || (!asSticker && !asEmoji)}>{busy ? 'Copying…' : 'Copy'}</button>
110
      </div>
111
    </fieldset>
112
  </form>
113
</div>
114
115
<style>
116
  .clone { width: min(460px, 100%); }
117
  h2 { margin: 0 0 16px; font-size: 18px; }
118
  fieldset { display: flex; flex-direction: column; gap: 14px; border: 0; padding: 0; margin: 0; min-width: 0; }
119
  .field { display: flex; flex-direction: column; gap: 6px; }
120
  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); }
121
  p { margin: 0; font-size: 13px; color: var(--muted); }
122
  .uses, .actions { display: flex; flex-wrap: wrap; gap: 14px; }
123
  .uses label { display: flex; align-items: center; gap: 6px; font-size: 13px; }
124
  .actions { justify-content: flex-end; }
125
  .error { color: var(--danger); }
126
</style>