feat: gallery for each category
This commit is contained in:
@ -4,14 +4,14 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
import { readFile } from "@tauri-apps/plugin-fs";
|
import { readFile } from "@tauri-apps/plugin-fs";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
type ParentProps,
|
type ParentProps,
|
||||||
createEffect,
|
createEffect,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import {
|
import {
|
||||||
type ShareEvent,
|
type ShareEvent,
|
||||||
listenForShareEvents,
|
listenForShareEvents,
|
||||||
} from "tauri-plugin-sharetarget-api";
|
} from "tauri-plugin-sharetarget-api";
|
||||||
import { Login } from "./Login";
|
import { Login } from "./Login";
|
||||||
import { ProtectedRoute } from "./ProtectedRoute";
|
import { ProtectedRoute } from "./ProtectedRoute";
|
||||||
@ -22,83 +22,83 @@ import { SearchImageContextProvider } from "./contexts/SearchImageContext";
|
|||||||
import { sendImageFile } from "./network";
|
import { sendImageFile } from "./network";
|
||||||
import { Image } from "./Image";
|
import { Image } from "./Image";
|
||||||
import { WithEntityDialog } from "./WithEntityDialog";
|
import { WithEntityDialog } from "./WithEntityDialog";
|
||||||
|
import { Gallery } from "./gallery";
|
||||||
|
|
||||||
const currentPlatform = platform();
|
const currentPlatform = platform();
|
||||||
console.log("Current Platform: ", currentPlatform);
|
console.log("Current Platform: ", currentPlatform);
|
||||||
|
|
||||||
const AppWrapper: Component<ParentProps> = ({ children }) => {
|
const AppWrapper: Component<ParentProps> = ({ children }) => {
|
||||||
return <div class="flex w-full justify-center h-screen">{children}</div>;
|
return <div class="flex w-full justify-center h-screen">{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
// TODO: Don't use window.location.href
|
// TODO: Don't use window.location.href
|
||||||
const unlisten = listen("focus-search", () => {
|
const unlisten = listen("focus-search", () => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
});
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
unlisten.then((fn) => fn());
|
unlisten.then((fn) => fn());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (currentPlatform !== "android") {
|
if (currentPlatform !== "android") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listener: PluginListener;
|
let listener: PluginListener;
|
||||||
|
|
||||||
const setupListener = async () => {
|
const setupListener = async () => {
|
||||||
console.log("Setting up listener");
|
console.log("Setting up listener");
|
||||||
|
|
||||||
listener = await listenForShareEvents(
|
listener = await listenForShareEvents(async (intent: ShareEvent) => {
|
||||||
async (intent: ShareEvent) => {
|
console.log(intent);
|
||||||
console.log(intent);
|
const contents = await readFile(intent.stream ?? "").catch(
|
||||||
const contents = await readFile(intent.stream ?? "").catch(
|
(error: Error) => {
|
||||||
(error: Error) => {
|
console.warn("fetching shared content failed:");
|
||||||
console.warn("fetching shared content failed:");
|
throw error;
|
||||||
throw error;
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const f = new File([contents], intent.name ?? "no-name", {
|
const f = new File([contents], intent.name ?? "no-name", {
|
||||||
type: intent.content_type,
|
type: intent.content_type,
|
||||||
});
|
});
|
||||||
|
|
||||||
sendImageFile(f.name, f);
|
sendImageFile(f.name, f);
|
||||||
},
|
});
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
setupListener();
|
setupListener();
|
||||||
return () => {
|
return () => {
|
||||||
listener?.unregister();
|
listener?.unregister();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchImageContextProvider>
|
<SearchImageContextProvider>
|
||||||
<ImageViewer />
|
<ImageViewer />
|
||||||
<Router>
|
<Router>
|
||||||
<Route path="/" component={AppWrapper}>
|
<Route path="/" component={AppWrapper}>
|
||||||
<Route path="/login" component={Login} />
|
<Route path="/login" component={Login} />
|
||||||
|
|
||||||
<Route path="/" component={ProtectedRoute}>
|
<Route path="/" component={ProtectedRoute}>
|
||||||
<Route path="/" component={WithEntityDialog}>
|
<Route path="/" component={WithEntityDialog}>
|
||||||
<Route path="/" component={Search} />
|
<Route path="/" component={Search} />
|
||||||
<Route path="/image/:imageId" component={Image} />
|
<Route path="/image/:imageId" component={Image} />
|
||||||
</Route>
|
<Route path="/gallery/:entity" component={Gallery} />
|
||||||
<Route path="/settings" component={Settings} />
|
</Route>
|
||||||
</Route>
|
<Route path="/settings" component={Settings} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
</Route>
|
||||||
path="*"
|
<Route
|
||||||
component={() => {
|
path="*"
|
||||||
return <Navigate href="/" />;
|
component={() => {
|
||||||
}}
|
return <Navigate href="/" />;
|
||||||
/>
|
}}
|
||||||
</Router>
|
/>
|
||||||
</SearchImageContextProvider>
|
</Router>
|
||||||
);
|
</SearchImageContextProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Component, createEffect, For } from "solid-js";
|
import { Component, For } from "solid-js";
|
||||||
import {
|
import {
|
||||||
SearchImageStore,
|
SearchImageStore,
|
||||||
useSearchImageContext,
|
useSearchImageContext,
|
||||||
} from "../contexts/SearchImageContext";
|
} from "../contexts/SearchImageContext";
|
||||||
|
import { A } from "@solidjs/router";
|
||||||
|
|
||||||
|
// TODO: lots of stuff to do with Entities, this could be seperated into a centralized place.
|
||||||
const CategoryColor: Record<
|
const CategoryColor: Record<
|
||||||
keyof ReturnType<SearchImageStore["categories"]>,
|
keyof ReturnType<SearchImageStore["categories"]>,
|
||||||
string
|
string
|
||||||
@ -21,7 +23,8 @@ export const Categories: Component = () => {
|
|||||||
<div class="w-full grid grid-cols-4 auto-rows-[minmax(100px,1fr)] gap-4 rounded-xl bg-white p-4">
|
<div class="w-full grid grid-cols-4 auto-rows-[minmax(100px,1fr)] gap-4 rounded-xl bg-white p-4">
|
||||||
<For each={Object.entries(categories())}>
|
<For each={Object.entries(categories())}>
|
||||||
{([category, group]) => (
|
{([category, group]) => (
|
||||||
<div
|
<A
|
||||||
|
href={`/gallery/${category}`}
|
||||||
class={
|
class={
|
||||||
"col-span-2 flex flex-col justify-center items-center rounded-lg p-4 border border-neutral-200 " +
|
"col-span-2 flex flex-col justify-center items-center rounded-lg p-4 border border-neutral-200 " +
|
||||||
"capitalize " +
|
"capitalize " +
|
||||||
@ -32,7 +35,7 @@ export const Categories: Component = () => {
|
|||||||
>
|
>
|
||||||
<p class="text-xl font-bold">{category}s</p>
|
<p class="text-xl font-bold">{category}s</p>
|
||||||
<p class="text-lg">{group.length}</p>
|
<p class="text-lg">{group.length}</p>
|
||||||
</div>
|
</A>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
50
frontend/src/gallery/index.tsx
Normal file
50
frontend/src/gallery/index.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Component, For, Show } from "solid-js";
|
||||||
|
import { useParams } from "@solidjs/router";
|
||||||
|
import { union, literal, safeParse, InferOutput, parse } from "valibot";
|
||||||
|
import { useSearchImageContext } from "../contexts/SearchImageContext";
|
||||||
|
import { SearchCard } from "../components/search-card/SearchCard";
|
||||||
|
|
||||||
|
const entityValidator = union([
|
||||||
|
literal("event"),
|
||||||
|
literal("note"),
|
||||||
|
literal("location"),
|
||||||
|
literal("contact"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const EntityGallery: Component<{
|
||||||
|
entity: InferOutput<typeof entityValidator>;
|
||||||
|
}> = (props) => {
|
||||||
|
// Just to be doubly sure.
|
||||||
|
parse(entityValidator, props.entity);
|
||||||
|
|
||||||
|
// These names are being silly. Entity or Category?
|
||||||
|
const { images } = useSearchImageContext();
|
||||||
|
|
||||||
|
const filteredCategories = () =>
|
||||||
|
images().filter((i) => i.type === props.entity);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col gap-4 capitalize">
|
||||||
|
<h2>{props.entity}s</h2>
|
||||||
|
<div class="grid grid-cols-3">
|
||||||
|
<For each={filteredCategories()}>
|
||||||
|
{(category) => <SearchCard item={category} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Gallery: Component = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const validated = safeParse(entityValidator, params.entity);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={validated.success}
|
||||||
|
fallback={<p>Sorry, this entity is not supported</p>}
|
||||||
|
>
|
||||||
|
<EntityGallery entity={validated.output as any} />
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user