feat: frontend receiving processing images and showing them as such

This commit is contained in:
2025-05-10 17:39:09 +01:00
parent 71dfe5647e
commit a4b94fc6c2
7 changed files with 49 additions and 102 deletions

View File

@ -3,6 +3,7 @@ package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"os"
@ -151,6 +152,7 @@ func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
// EG: The user could attempt to create many connections
// and they just get a 500, with no explanation.
w.WriteHeader(http.StatusInternalServerError)
w.(http.Flusher).Flush()
return
}
@ -163,8 +165,15 @@ func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
w.(http.Flusher).Flush()
return
case msg := <-listener:
msgString, err := json.Marshal(msg)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Printf("Sending msg %s\n", msg)
fmt.Fprintf(w, "event: data\ndata: %s\n\n", msg)
fmt.Fprintf(w, "event: data\ndata: %s\n\n", string(msgString))
w.(http.Flusher).Flush()
}
}

View File

@ -19,7 +19,6 @@ import { ProtectedRoute } from "./ProtectedRoute";
import { Search } from "./Search";
import { Settings } from "./Settings";
import { ImageViewer } from "./components/ImageViewer";
import { ImageStatus } from "./components/image-status/ImageStatus";
import { SearchImageContextProvider } from "./contexts/SearchImageContext";
import { type sendImage, sendImageFile } from "./network";
import { Image } from "./Image";
@ -86,10 +85,6 @@ export const App = () => {
return (
<SearchImageContextProvider>
<ImageViewer onSendImage={setProcessingImage} />
<ImageStatus
processingImage={processingImage}
onSetProcessingImage={setProcessingImage}
/>
<Router>
<Route path="/" component={AppWrapper}>
<Route path="/login" component={Login} />

View File

@ -1,14 +1,8 @@
import {
type Component,
type JSX,
ParentProps,
Show,
useContext,
} from "solid-js";
import { jwtDecode } from "jwt-decode";
import { Navigate } from "@solidjs/router";
import { save_token } from "tauri-plugin-ios-shared-token-api";
import { platform } from "@tauri-apps/plugin-os";
import { jwtDecode } from "jwt-decode";
import { type Component, type ParentProps, Show, useContext } from "solid-js";
import { save_token } from "tauri-plugin-ios-shared-token-api";
import { Notifications, NotificationsContext } from "./notifications";
export const isTokenValid = (): boolean => {

View File

@ -168,6 +168,8 @@ export const Search = () => {
<ProcessingImages />
<h2 class="text-xl">Images</h2>
<div class="w-full grid grid-cols-9 gap-2 grid-flow-row-dense py-4">
<For each={Object.keys(imagesWithProperties())}>
{(imageId) => (

View File

@ -1,67 +0,0 @@
import { createEffect, type Accessor, type Component } from "solid-js";
import { base, type sendImage } from "../../network";
import { useSearchImageContext } from "../../contexts/SearchImageContext";
type ImageStatusProps = {
onSetProcessingImage: (
image: Awaited<ReturnType<typeof sendImage>> | undefined,
) => void;
processingImage: Accessor<
Awaited<ReturnType<typeof sendImage>> | undefined
>;
};
type EventData = "in-progress" | "complete";
export const ImageStatus: Component<ImageStatusProps> = (props) => {
const { onRefetchImages } = useSearchImageContext();
let processing = false;
const onEvent = (e: MessageEvent<EventData>) => {
console.log("Processing Events: ", e.data);
const processingImage = props.processingImage();
if (processingImage == null) {
throw new Error("Processing Image should not be null");
}
if (e.data !== "complete") {
props.onSetProcessingImage({
...processingImage,
Status: e.data,
});
processing = false;
return;
}
props.onSetProcessingImage(undefined);
onRefetchImages();
};
createEffect(() => {
const image = props.processingImage();
if (image == null) {
return;
}
if (processing) {
return;
}
processing = true;
const eventSourceUrl = `${base}/image-events/${image.ID}`;
const eventSource = new EventSource(eventSourceUrl);
eventSource.addEventListener("data", onEvent);
return () => {
eventSource.removeEventListener("data", onEvent);
};
});
return null;
};

View File

@ -1,13 +1,16 @@
import { type Component, For } from "solid-js";
import { useNotifications } from "../ProtectedRoute";
import { base } from "../network";
import { useNotifications } from "../ProtectedRoute";
export const ProcessingImages: Component = () => {
const notifications = useNotifications();
return (
<For each={Object.entries(notifications.state.ProcessingImages)}>
{([id]) => <img alt="processing" src={`${base}/image/${id}`} />}
</For>
<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>
);
};

View File

@ -10,17 +10,17 @@ import {
uuid,
} from "valibot";
import { base } from "../network";
import { createContext } from "solid-js";
import { createContext, onCleanup } from "solid-js";
const processingImagesValidator = strictObject({
ImageID: pipe(string(), uuid()),
Status: union([literal("in-process"), literal("complete")]),
Status: union([literal("in-progress"), literal("complete")]),
});
type NotificationState = {
ProcessingImages: Record<
string,
InferOutput<typeof processingImagesValidator>["Status"]
InferOutput<typeof processingImagesValidator>["Status"] | undefined
>;
};
@ -35,7 +35,20 @@ export const Notifications = () => {
}
const dataEventListener = (e: MessageEvent<unknown>) => {
const processingImage = safeParse(processingImagesValidator, e.data);
if (typeof e.data !== "string") {
console.error("Error type is not string");
return;
}
let jsonData: object = {};
try {
jsonData = JSON.parse(e.data);
} catch (e) {
console.error(e);
return;
}
const processingImage = safeParse(processingImagesValidator, jsonData);
if (!processingImage.success) {
console.error("Processing image could not be parsed.", e.data);
return;
@ -43,32 +56,25 @@ export const Notifications = () => {
const { ImageID, Status } = processingImage.output;
if (ImageID in state.ProcessingImages) {
if (state.ProcessingImages[ImageID] != null) {
const localImageStatus = state.ProcessingImages[ImageID];
if (localImageStatus !== "in-process" || Status !== "complete") {
if (localImageStatus !== "in-progress" || Status !== "complete") {
console.error(
"Invalid state, an image present must always be in process",
);
return;
}
setState("ProcessingImages", (images) => {
delete images[ImageID];
// TODO: this might not work because it's not a new object.
return images;
});
setState("ProcessingImages", ImageID, undefined);
} else {
if (Status !== "in-process") {
if (Status !== "in-progress") {
console.error(
"Invalid state, at this point a new image that isn't present should be in-progress",
);
return;
}
setState("ProcessingImages", {
[ImageID]: Status,
});
setState("ProcessingImages", ImageID, Status);
}
};
@ -76,6 +82,11 @@ export const Notifications = () => {
events.addEventListener("data", dataEventListener);
onCleanup(() => {
events.removeEventListener("data", dataEventListener);
events.close();
});
return {
state,
};