From 5d66dbceb743f0e9c948604737319d0fed3e4386 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 1 Mar 2026 18:46:49 +0200 Subject: [PATCH] better media setup --- .../(main)/devices/[deviceId]/+page.svelte | 154 ++++++++++++++---- packages/logic/domains/mobile/data.ts | 1 + packages/logic/domains/mobile/repository.ts | 19 ++- 3 files changed, 142 insertions(+), 32 deletions(-) diff --git a/apps/main/src/routes/(main)/devices/[deviceId]/+page.svelte b/apps/main/src/routes/(main)/devices/[deviceId]/+page.svelte index 242d7b1..184e47a 100644 --- a/apps/main/src/routes/(main)/devices/[deviceId]/+page.svelte +++ b/apps/main/src/routes/(main)/devices/[deviceId]/+page.svelte @@ -3,20 +3,47 @@ import Icon from "$lib/components/atoms/icon.svelte"; import { Button } from "$lib/components/ui/button"; import * as Card from "$lib/components/ui/card"; + import * as Dialog from "$lib/components/ui/dialog/index.js"; import * as Table from "$lib/components/ui/table"; import * as Tabs from "$lib/components/ui/tabs/index.js"; + import type { MobileMediaAsset } from "@pkg/logic/domains/mobile/data"; import { mainNavTree } from "$lib/core/constants"; import { mobileVM } from "$lib/domains/mobile/mobile.vm.svelte"; import { breadcrumbs } from "$lib/global.stores"; import ArrowLeft from "@lucide/svelte/icons/arrow-left"; + import FileIcon from "@lucide/svelte/icons/file"; import ImageIcon from "@lucide/svelte/icons/image"; import MessageSquare from "@lucide/svelte/icons/message-square"; import Smartphone from "@lucide/svelte/icons/smartphone"; + import VideoIcon from "@lucide/svelte/icons/video"; import { onDestroy, onMount } from "svelte"; let selectedTab = $state("sms"); + let mediaPreviewOpen = $state(false); + let selectedMedia = $state(null as MobileMediaAsset | null); const deviceId = $derived(Number(page.params.deviceId)); + function isImageAsset(asset: MobileMediaAsset) { + return asset.mimeType.startsWith("image/"); + } + + function isVideoAsset(asset: MobileMediaAsset) { + return asset.mimeType.startsWith("video/"); + } + + function formatSize(size: number | null | undefined) { + if (!size || size <= 0) return "-"; + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; + if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`; + return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`; + } + + function openMediaPreview(asset: MobileMediaAsset) { + selectedMedia = asset; + mediaPreviewOpen = true; + } + breadcrumbs.set([ mainNavTree[0], { title: "Device Detail", url: page.url.pathname }, @@ -160,39 +187,106 @@ No media assets yet. {:else} - - - - Filename - MIME - Size - Captured - File ID - - - - {#each mobileVM.media as asset (asset.id)} - - {asset.filename || "-"} - {asset.mimeType} - - {asset.sizeBytes ? `${asset.sizeBytes} B` : "-"} - - - {asset.capturedAt - ? new Date(asset.capturedAt).toLocaleString() - : "-"} - - - {asset.fileId} - - - {/each} - - +
+ {#each mobileVM.media as asset (asset.id)} + + {/each} +
{/if} + + + + {#if selectedMedia} + + {selectedMedia.filename || "Media Preview"} + {selectedMedia.mimeType} + + +
+ {#if isImageAsset(selectedMedia) && selectedMedia.r2Url} + {selectedMedia.filename + {:else if isVideoAsset(selectedMedia)} +
+
+ +

Video preview opens in a new tab.

+
+
+ {:else} +
+
+ +

Preview unavailable for this file type.

+
+
+ {/if} +
+ + + {#if selectedMedia.r2Url} + + {/if} + + {/if} +
+
diff --git a/packages/logic/domains/mobile/data.ts b/packages/logic/domains/mobile/data.ts index bcd6cbc..bc15f86 100644 --- a/packages/logic/domains/mobile/data.ts +++ b/packages/logic/domains/mobile/data.ts @@ -71,6 +71,7 @@ export const mobileMediaAssetSchema = v.object({ deviceId: v.pipe(v.number(), v.integer()), externalMediaId: v.optional(v.nullable(v.string())), fileId: v.string(), + r2Url: v.optional(v.nullable(v.string())), mimeType: v.string(), filename: v.optional(v.nullable(v.string())), capturedAt: v.optional(v.nullable(v.date())), diff --git a/packages/logic/domains/mobile/repository.ts b/packages/logic/domains/mobile/repository.ts index 6972e2f..ccc4d71 100644 --- a/packages/logic/domains/mobile/repository.ts +++ b/packages/logic/domains/mobile/repository.ts @@ -8,7 +8,7 @@ import { like, or, } from "@pkg/db"; -import { mobileDevice, mobileMediaAsset, mobileSMS, user } from "@pkg/db/schema"; +import { file, mobileDevice, mobileMediaAsset, mobileSMS, user } from "@pkg/db/schema"; import { ResultAsync, errAsync, okAsync } from "neverthrow"; import { FlowExecCtx } from "@core/flow.execution.context"; import type { @@ -647,8 +647,23 @@ export class MobileRepository { ).andThen((countRows) => ResultAsync.fromPromise( this.db - .select() + .select({ + id: mobileMediaAsset.id, + deviceId: mobileMediaAsset.deviceId, + externalMediaId: mobileMediaAsset.externalMediaId, + fileId: mobileMediaAsset.fileId, + r2Url: file.r2Url, + mimeType: mobileMediaAsset.mimeType, + filename: mobileMediaAsset.filename, + capturedAt: mobileMediaAsset.capturedAt, + sizeBytes: mobileMediaAsset.sizeBytes, + hash: mobileMediaAsset.hash, + metadata: mobileMediaAsset.metadata, + createdAt: mobileMediaAsset.createdAt, + updatedAt: mobileMediaAsset.updatedAt, + }) .from(mobileMediaAsset) + .leftJoin(file, eq(mobileMediaAsset.fileId, file.id)) .where(whereClause) .orderBy(orderFn(mobileMediaAsset.createdAt), desc(mobileMediaAsset.id)) .limit(pageSize)