summaryrefslogtreecommitdiffhomepage
path: root/src/App.svelte
blob: a9a250621d70b0423e9b53b381703ed7e05a2ffd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<script lang="ts">
  import { flashair } from './lib/flashair';
  import type { FlashAirFileEntry } from './lib/flashair';
  import ImageList from './lib/components/ImageList.svelte';
  import ImagePreview from './lib/components/ImagePreview.svelte';

  let images = $state<FlashAirFileEntry[]>([]);
  let selectedFile = $state<FlashAirFileEntry | undefined>(undefined);
  let loading = $state(false);
  let error = $state<string | undefined>(undefined);
  let isDark = $state(false);

  $effect(() => {
    document.documentElement.setAttribute('data-theme', isDark ? 'black' : 'cmyk');
  });

  async function loadAllImages() {
    loading = true;
    error = undefined;
    try {
      images = await flashair.listAllImages('/DCIM');
      if (images.length > 0 && selectedFile === undefined) {
        selectedFile = images[0];
      }
    } catch (e) {
      error = e instanceof Error ? e.message : String(e);
      images = [];
    } finally {
      loading = false;
    }
  }

  function selectImage(file: FlashAirFileEntry) {
    selectedFile = file;
  }
</script>

<div class="flex h-screen bg-base-200">
  <!-- Left: Image preview -->
  <div class="flex-1 min-w-0 h-full overflow-hidden">
    {#if loading}
      <div class="flex items-center justify-center h-full">
        <span class="loading loading-spinner loading-lg"></span>
      </div>
    {:else if error}
      <div class="flex items-center justify-center h-full p-4">
        <div role="alert" class="alert alert-error max-w-md">
          <span>{error}</span>
          <button class="btn btn-sm" onclick={() => loadAllImages()}>Retry</button>
        </div>
      </div>
    {:else if images.length === 0}
      <div class="flex items-center justify-center h-full text-base-content/60 p-8">
        <div class="text-center">
          <p>No photos found. Connect to FlashAir WiFi and tap the refresh button.</p>
          <button class="btn btn-primary mt-4" onclick={() => loadAllImages()}>Load Photos</button>
        </div>
      </div>
    {:else}
      <ImagePreview file={selectedFile} />
    {/if}
  </div>

  <!-- Right: Image list -->
<div class="w-36 lg:w-40 shrink-0 border-l border-base-300 flex flex-col">
    <div class="px-3 py-2 bg-base-100 border-b border-base-300 shrink-0">
      <span class="text-sm font-semibold">Photos ({images.length})</span>
    </div>
    <div class="flex-1 min-h-0">
      <ImageList {images} selectedPath={selectedFile?.path} onSelect={selectImage} />
    </div>
  </div>
</div>

<!-- FAB Flower: bottom-right -->
<div class="fab fab-flower">
  <div tabindex="0" class="btn btn-lg btn-circle btn-primary">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
      <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
    </svg>
  </div>
  <div class="fab-close">
    <span class="btn btn-circle btn-lg btn-secondary">✕</span>
  </div>
  <!-- Dark mode toggle -->
  <button class="btn btn-lg btn-circle btn-secondary" onclick={() => (isDark = !isDark)}>
    {#if isDark}
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
        <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
      </svg>
    {:else}
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
        <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
      </svg>
    {/if}
  </button>
  <!-- Refresh -->
  <button class="btn btn-lg btn-circle btn-secondary" onclick={() => loadAllImages()}>
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
      <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12a7.5 7.5 0 0 1 12.57-5.55L19.5 8.87" />
      <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 4.5v4.37h-4.37" />
      <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12a7.5 7.5 0 0 1-12.57 5.55L4.5 15.13" />
      <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5v-4.37h4.37" />
    </svg>
  </button>
</div>