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 {
ImageID uuid.UUID
Status string
ImageID uuid.UUID
ImageName string
Status string
}
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)
notification := Notification{
ImageID: processingImage.ImageID,
Status: status,
ImageID: processingImage.ImageID,
ImageName: processingImage.Image.ImageName,
Status: status,
}
if err := notifier.SendAndCreate(processingImage.UserID.String(), notification); err != nil {

View File

@ -3,7 +3,6 @@ package models
import (
"context"
"database/sql"
"errors"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/enum"
"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
}
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (model.UserImagesToProcess, error) {
getToProcessStmt := UserImagesToProcess.
SELECT(UserImagesToProcess.AllColumns).
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (UserProcessingImage, error) {
getToProcessStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
FROM(
UserImagesToProcess.INNER_JOIN(
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
),
).
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []model.UserImagesToProcess{}
images := []UserProcessingImage{}
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
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

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 { 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 = () => {
const notifications = useNotifications();
const notifications = useNotifications();
return (
<div class="w-full">
<h2 class="text-xl">Processing Images</h2>
<For each={Object.entries(notifications.state.ProcessingImages)}>
{([id]) => <img alt="processing" src={`${base}/image/${id}`} />}
</For>
</div>
);
return (
<Popover>
<Popover.Trigger class="w-full flex justify-between rounded-xl bg-slate-800 text-gray-100 px-4 py-2">
<p class="text-md">Processing Images</p>
<Show
when={Object.keys(notifications.state.ProcessingImages).length === 0}
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({
ImageID: pipe(string(), uuid()),
ImageName: string(),
Status: union([
literal("not-started"),
literal("in-progress"),
@ -25,7 +26,7 @@ const processingImagesValidator = strictObject({
type NotificationState = {
ProcessingImages: Record<
string,
InferOutput<typeof processingImagesValidator>["Status"] | undefined
InferOutput<typeof processingImagesValidator> | undefined
>;
};
@ -69,7 +70,7 @@ export const Notifications = (onCompleteImage: () => void) => {
setState("ProcessingImages", ImageID, undefined);
onCompleteImage();
} else {
setState("ProcessingImages", ImageID, Status);
setState("ProcessingImages", ImageID, processingImage.output);
}
};
@ -89,7 +90,16 @@ export const Notifications = (onCompleteImage: () => void) => {
}
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,
},
]),
),
);
});