feat: processing images and notification centre

This commit is contained in:
2025-07-02 14:12:19 +01:00
parent c62378c20a
commit 6482a76a51
4 changed files with 108 additions and 23 deletions

View File

@ -18,8 +18,9 @@ import (
) )
type Notification struct { type Notification struct {
ImageID uuid.UUID ImageID uuid.UUID
Status string ImageName string
Status string
} }
func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) { func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) {
@ -116,8 +117,9 @@ func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier
logger.Info("Update", "id", imageStringUuid, "status", status) logger.Info("Update", "id", imageStringUuid, "status", status)
notification := Notification{ notification := Notification{
ImageID: processingImage.ImageID, ImageID: processingImage.ImageID,
Status: status, ImageName: processingImage.Image.ImageName,
Status: status,
} }
if err := notifier.SendAndCreate(processingImage.UserID.String(), notification); err != nil { if err := notifier.SendAndCreate(processingImage.UserID.String(), notification); err != nil {

View File

@ -3,7 +3,6 @@ package models
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"screenmark/screenmark/.gen/haystack/haystack/enum" "screenmark/screenmark/.gen/haystack/haystack/enum"
"screenmark/screenmark/.gen/haystack/haystack/model" "screenmark/screenmark/.gen/haystack/haystack/model"
@ -69,16 +68,20 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
return userImage, err return userImage, err
} }
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (model.UserImagesToProcess, error) { func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (UserProcessingImage, error) {
getToProcessStmt := UserImagesToProcess. getToProcessStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
SELECT(UserImagesToProcess.AllColumns). FROM(
UserImagesToProcess.INNER_JOIN(
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
),
).
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId))) WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []model.UserImagesToProcess{} images := []UserProcessingImage{}
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images) err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 { if len(images) != 1 {
return model.UserImagesToProcess{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images))) return UserProcessingImage{}, fmt.Errorf("Expected 1, got %d\n", len(images))
} }
return images[0], err return images[0], err

View File

@ -1,16 +1,86 @@
import { type Component, For } from "solid-js"; import { Popover } from "@kobalte/core/popover";
import { type Component, For, Show } from "solid-js";
import { base } from "../network"; import { base } from "../network";
import { useNotifications } from "../ProtectedRoute"; import { useNotifications } from "../ProtectedRoute";
import { useSearchImageContext } from "../contexts/SearchImageContext";
const LoadingCircle: Component<{ status: "loading" | "complete" }> = (
props,
) => {
switch (props.status) {
case "loading":
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke-width="3"
class="stroke-amber-400"
/>
</svg>
);
case "complete":
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke-width="3"
class="stroke-emerald-400"
/>
</svg>
);
}
};
export const ProcessingImages: Component = () => { export const ProcessingImages: Component = () => {
const notifications = useNotifications(); const notifications = useNotifications();
return ( return (
<div class="w-full"> <Popover>
<h2 class="text-xl">Processing Images</h2> <Popover.Trigger class="w-full flex justify-between rounded-xl bg-slate-800 text-gray-100 px-4 py-2">
<For each={Object.entries(notifications.state.ProcessingImages)}> <p class="text-md">Processing Images</p>
{([id]) => <img alt="processing" src={`${base}/image/${id}`} />} <Show
</For> when={Object.keys(notifications.state.ProcessingImages).length === 0}
</div> fallback={<LoadingCircle status="loading" />}
); >
<LoadingCircle status="complete" />
</Show>
</Popover.Trigger>
<Popover.Content class="w-96 flex flex-col gap-2 bg-slate-800 rounded-xl">
<For each={Object.entries(notifications.state.ProcessingImages)}>
{([id, _image]) => (
<Show when={_image}>
{(image) => (
<div class="flex gap-2">
<img
class="w-16 h-16 aspect-square"
alt="processing"
src={`${base}/image/${id}`}
/>
<div class="flex flex-col gap-1">
<p class="text-slate-100">{image().ImageName}</p>
</div>
</div>
)}
</Show>
)}
</For>
</Popover.Content>
</Popover>
);
}; };

View File

@ -15,6 +15,7 @@ import { useSearchImageContext } from "../contexts/SearchImageContext";
const processingImagesValidator = strictObject({ const processingImagesValidator = strictObject({
ImageID: pipe(string(), uuid()), ImageID: pipe(string(), uuid()),
ImageName: string(),
Status: union([ Status: union([
literal("not-started"), literal("not-started"),
literal("in-progress"), literal("in-progress"),
@ -25,7 +26,7 @@ const processingImagesValidator = strictObject({
type NotificationState = { type NotificationState = {
ProcessingImages: Record< ProcessingImages: Record<
string, string,
InferOutput<typeof processingImagesValidator>["Status"] | undefined InferOutput<typeof processingImagesValidator> | undefined
>; >;
}; };
@ -69,7 +70,7 @@ export const Notifications = (onCompleteImage: () => void) => {
setState("ProcessingImages", ImageID, undefined); setState("ProcessingImages", ImageID, undefined);
onCompleteImage(); onCompleteImage();
} else { } else {
setState("ProcessingImages", ImageID, Status); setState("ProcessingImages", ImageID, processingImage.output);
} }
}; };
@ -89,7 +90,16 @@ export const Notifications = (onCompleteImage: () => void) => {
} }
upsertImageProcessing( upsertImageProcessing(
Object.fromEntries(images.map((i) => [i.ImageID, i.Status])), Object.fromEntries(
images.map((i) => [
i.ImageID,
{
ImageID: i.ImageID,
ImageName: i.Image.ImageName,
Status: i.Status,
},
]),
),
); );
}); });