diff --git a/backend/.gen/haystack/haystack/enum/progress.go b/backend/.gen/haystack/haystack/enum/progress.go
index 1509f3f..c30e564 100644
--- a/backend/.gen/haystack/haystack/enum/progress.go
+++ b/backend/.gen/haystack/haystack/enum/progress.go
@@ -12,7 +12,9 @@ import "github.com/go-jet/jet/v2/postgres"
var Progress = &struct {
NotStarted postgres.StringExpression
InProgress postgres.StringExpression
+ Complete postgres.StringExpression
}{
NotStarted: postgres.NewEnumValue("not-started"),
InProgress: postgres.NewEnumValue("in-progress"),
+ Complete: postgres.NewEnumValue("complete"),
}
diff --git a/backend/.gen/haystack/haystack/model/progress.go b/backend/.gen/haystack/haystack/model/progress.go
index 968b5c0..c674a42 100644
--- a/backend/.gen/haystack/haystack/model/progress.go
+++ b/backend/.gen/haystack/haystack/model/progress.go
@@ -14,11 +14,13 @@ type Progress string
const (
Progress_NotStarted Progress = "not-started"
Progress_InProgress Progress = "in-progress"
+ Progress_Complete Progress = "complete"
)
var ProgressAllValues = []Progress{
Progress_NotStarted,
Progress_InProgress,
+ Progress_Complete,
}
func (e *Progress) Scan(value interface{}) error {
@@ -37,6 +39,8 @@ func (e *Progress) Scan(value interface{}) error {
*e = Progress_NotStarted
case "in-progress":
*e = Progress_InProgress
+ case "complete":
+ *e = Progress_Complete
default:
return errors.New("jet: Invalid scan value '" + enumValue + "' for Progress enum")
}
diff --git a/backend/events.go b/backend/events.go
index fd23f03..6eda913 100644
--- a/backend/events.go
+++ b/backend/events.go
@@ -68,11 +68,11 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
_, err = imageModel.FinishProcessing(ctx, image.ID)
if err != nil {
- databaseEventLog.Error("Failed to finish processing", "ImageID", imageId)
+ databaseEventLog.Error("Failed to finish processing", "ImageID", imageId, "error", err)
return
}
- databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
+ databaseEventLog.Debug("Finished processing image", "ImageID", imageId)
}()
}
}
@@ -119,8 +119,12 @@ func ListenProcessingImageStatus(db *sql.DB, eventManager *EventManager) {
logger.Info("Sending...")
imageListener <- status
- // close(imageListener)
- // delete(eventManager.listeners, stringUuid)
+ if status != "complete" {
+ continue
+ }
+
+ close(imageListener)
+ delete(eventManager.listeners, stringUuid)
}
}
}
diff --git a/backend/main.go b/backend/main.go
index 42caedc..f5190a5 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -13,7 +13,6 @@ import (
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
- "time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -267,6 +266,8 @@ func main() {
w.Header().Set("Connection", "keep-alive")
w.(http.Flusher).Flush()
+ // TODO: get the current status of the image and send it across.
+
ctx, cancel := context.WithCancel(r.Context())
for {
@@ -277,11 +278,18 @@ func main() {
cancel()
return
case data := <-imageNotifier:
+ if data == "" {
+ cancel()
+ continue
+ }
+
fmt.Printf("Status received: %s\n", data)
- fmt.Fprintf(w, "event: data\ndata: %s-%s\n\n", data, time.Now().String())
+ fmt.Fprintf(w, "event: data\ndata: %s\n\n", data)
w.(http.Flusher).Flush()
- cancel()
- return
+
+ if data == "complete" {
+ cancel()
+ }
}
}
})
diff --git a/backend/models/image.go b/backend/models/image.go
index e5abe31..e3daa10 100644
--- a/backend/models/image.go
+++ b/backend/models/image.go
@@ -117,6 +117,19 @@ func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (mo
return model.UserImages{}, err
}
+ // Hacky. Update the status before removing so we can get our regular triggers
+ // to work.
+
+ updateStatusStmt := UserImagesToProcess.
+ UPDATE(UserImagesToProcess.Status).
+ SET(model.Progress_Complete).
+ WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
+
+ _, err = updateStatusStmt.ExecContext(ctx, tx)
+ if err != nil {
+ return model.UserImages{}, err
+ }
+
removeProcessingStmt := UserImagesToProcess.
DELETE().
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
diff --git a/backend/schema.sql b/backend/schema.sql
index f074a2b..daff7e2 100644
--- a/backend/schema.sql
+++ b/backend/schema.sql
@@ -4,7 +4,7 @@ CREATE SCHEMA haystack;
/* -----| Enums |----- */
-CREATE TYPE haystack.progress AS ENUM('not-started','in-progress');
+CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
/* -----| Schema tables |----- */
diff --git a/frontend/src-tauri/capabilities/desktop.toml b/frontend/src-tauri/capabilities/desktop.toml
index 9b6ac60..5243f9b 100644
--- a/frontend/src-tauri/capabilities/desktop.toml
+++ b/frontend/src-tauri/capabilities/desktop.toml
@@ -7,6 +7,7 @@ permissions = [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-show",
+ "core:window:allow-set-focus",
"fs:default",
{ identifier = "http:default", allow = [
{ url = "https://haystack.johncosta.tech" },
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 56edfa7..af704d4 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -6,10 +6,10 @@ import { ProtectedRoute } from "./ProtectedRoute";
import { Search } from "./Search";
import { Settings } from "./Settings";
import { ImageViewer } from "./components/ImageViewer";
-import { ShareTarget } from "./components/share-target/ShareTarget";
import type { sendImage } from "./network";
import { ImageStatus } from "./components/image-status/ImageStatus";
import { invoke } from "@tauri-apps/api/core";
+import { SearchImageContextProvider } from "./contexts/SearchImageContext";
export const App = () => {
const [processingImage, setProcessingImage] =
@@ -57,7 +57,7 @@ export const App = () => {
};
return (
- <>
+
@@ -71,6 +71,6 @@ export const App = () => {
- >
+
);
};
diff --git a/frontend/src/Search.tsx b/frontend/src/Search.tsx
index 5e95c3d..4b25edd 100644
--- a/frontend/src/Search.tsx
+++ b/frontend/src/Search.tsx
@@ -1,52 +1,21 @@
import { Button } from "@kobalte/core/button";
-
import { IconSearch, IconSettings } from "@tabler/icons-solidjs";
import { listen } from "@tauri-apps/api/event";
-
import Fuse from "fuse.js";
import {
For,
Show,
createEffect,
- createResource,
createSignal,
onCleanup,
onMount,
} from "solid-js";
-
import { SearchCard } from "./components/search-card/SearchCard";
-
import { invoke } from "@tauri-apps/api/core";
import { ItemModal } from "./components/item-modal/ItemModal";
import type { Shortcut } from "./components/shortcuts/hooks/useShortcutEditor";
-import { type UserImage, getUserImages } from "./network";
-
-// How wonderfully functional
-const getAllValues = (object: object): Array => {
- const loop = (acc: Array, next: object): Array => {
- for (const _value of Object.values(next)) {
- const value: unknown = _value;
- switch (typeof value) {
- case "object":
- if (value != null) {
- acc.push(...loop(acc, value));
- }
- break;
- case "string":
- case "number":
- case "boolean":
- acc.push(value.toString());
- break;
- default:
- break;
- }
- }
-
- return acc;
- };
-
- return loop([], object);
-};
+import type { UserImage } from "./network";
+import { useSearchImageContext } from "./contexts/SearchImageContext";
export const Search = () => {
const [searchResults, setSearchResults] = createSignal([]);
@@ -55,17 +24,9 @@ export const Search = () => {
null,
);
- const [data] = createResource(() =>
- getUserImages().then((data) => {
- console.log("DBG: ", data);
- return data.map((d) => ({
- ...d,
- rawData: getAllValues(d),
- }));
- }),
- );
+ const { images } = useSearchImageContext();
- let fuze = new Fuse(data() ?? [], {
+ let fuze = new Fuse(images() ?? [], {
keys: [
{ name: "rawData", weight: 1 },
{ name: "title", weight: 1 },
@@ -74,10 +35,10 @@ export const Search = () => {
});
createEffect(() => {
- console.log("DBG: ", data());
- setSearchResults(data() ?? []);
+ console.log("DBG: ", images());
+ setSearchResults(images() ?? []);
- fuze = new Fuse(data() ?? [], {
+ fuze = new Fuse(images() ?? [], {
keys: [
{ name: "data.Name", weight: 2 },
{ name: "rawData", weight: 1 },
diff --git a/frontend/src/components/ImageViewer.tsx b/frontend/src/components/ImageViewer.tsx
index f32939f..447a80c 100644
--- a/frontend/src/components/ImageViewer.tsx
+++ b/frontend/src/components/ImageViewer.tsx
@@ -29,7 +29,6 @@ export const ImageViewer: Component = (props) => {
props.onSendImage(result);
- window.location.reload();
console.log("DBG: ", result);
});
diff --git a/frontend/src/components/image-status/ImageStatus.tsx b/frontend/src/components/image-status/ImageStatus.tsx
index 4be12d9..8781936 100644
--- a/frontend/src/components/image-status/ImageStatus.tsx
+++ b/frontend/src/components/image-status/ImageStatus.tsx
@@ -1,5 +1,6 @@
-import { Show, type Accessor, type Component } from "solid-js";
+import { createEffect, Show, type Accessor, type Component } from "solid-js";
import type { sendImage } from "../../network";
+import { useSearchImageContext } from "../../contexts/SearchImageContext";
type ImageStatusProps = {
processingImage: Accessor<
@@ -7,7 +8,38 @@ type ImageStatusProps = {
>;
};
+type EventData = "in-progress" | "complete";
+
export const ImageStatus: Component = (props) => {
+ const { onRefetchImages } = useSearchImageContext();
+
+ const onEvent = (e: MessageEvent) => {
+ console.log(e.data);
+
+ if (e.data !== "complete") {
+ return;
+ }
+
+ onRefetchImages();
+ };
+
+ createEffect(() => {
+ const image = props.processingImage();
+ if (image == null) {
+ return;
+ }
+
+ const eventSource = new EventSource(
+ `http://192.168.1.199:3040/image-events/${image.ID}`,
+ );
+
+ eventSource.addEventListener("data", onEvent);
+
+ return () => {
+ eventSource.removeEventListener("data", onEvent);
+ };
+ });
+
return (
{(image) => (
diff --git a/frontend/src/contexts/SearchImageContext.tsx b/frontend/src/contexts/SearchImageContext.tsx
new file mode 100644
index 0000000..a4c0427
--- /dev/null
+++ b/frontend/src/contexts/SearchImageContext.tsx
@@ -0,0 +1,79 @@
+import {
+ createContext,
+ type Resource,
+ type Component,
+ type ParentProps,
+ createResource,
+ useContext,
+} from "solid-js";
+import { getUserImages } from "../network";
+
+type ImageWithRawData = Awaited>[number] & {
+ rawData: string[];
+};
+
+type SearchImageStore = {
+ images: Resource;
+ onRefetchImages: () => void;
+};
+
+// How wonderfully functional
+const getAllValues = (object: object): Array => {
+ const loop = (acc: Array, next: object): Array => {
+ for (const _value of Object.values(next)) {
+ const value: unknown = _value;
+ switch (typeof value) {
+ case "object":
+ if (value != null) {
+ acc.push(...loop(acc, value));
+ }
+ break;
+ case "string":
+ case "number":
+ case "boolean":
+ acc.push(value.toString());
+ break;
+ default:
+ break;
+ }
+ }
+
+ return acc;
+ };
+
+ return loop([], object);
+};
+
+const SearchImageContext = createContext();
+export const SearchImageContextProvider: Component = (props) => {
+ const [images, { refetch }] = createResource(() =>
+ getUserImages().then((data) => {
+ return data.map((d) => ({
+ ...d,
+ rawData: getAllValues(d),
+ }));
+ }),
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const useSearchImageContext = () => {
+ const context = useContext(SearchImageContext);
+ if (context == null) {
+ throw new Error(
+ "Unreachable: We should always have a mounted context and no undefined values",
+ );
+ }
+
+ return context;
+};
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 0e8f6d5..10d83e5 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -3,19 +3,4 @@ import { render } from "solid-js/web";
import "./index.css";
import { App } from "./App";
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-console.log("Hello android!");
-
render(() => , document.getElementById("root") as HTMLElement);