feat: processing images and notification centre
This commit is contained in:
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user