refactor: commands from menu
This commit is contained in:
parent
b95cfcc5b4
commit
f9638c35fc
@ -15,7 +15,7 @@
|
||||
},
|
||||
"imports": {
|
||||
"$fresh/": "https://deno.land/x/fresh@1.3.1/",
|
||||
"yaml": "https://deno.land/std@0.196.0/yaml/parse.ts",
|
||||
"yaml": "https://deno.land/std@0.194.0/yaml/mod.ts",
|
||||
"preact": "https://esm.sh/preact@10.15.1",
|
||||
"preact/": "https://esm.sh/preact@10.15.1/",
|
||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
|
||||
|
10
fresh.gen.ts
10
fresh.gen.ts
@ -31,7 +31,10 @@ import * as $$0 from "./islands/Counter.tsx";
|
||||
import * as $$1 from "./islands/IngredientsList.tsx";
|
||||
import * as $$2 from "./islands/KMenu.tsx";
|
||||
import * as $$3 from "./islands/KMenu/commands.ts";
|
||||
import * as $$4 from "./islands/KMenu/types.ts";
|
||||
import * as $$4 from "./islands/KMenu/commands/add_movie_infos.ts";
|
||||
import * as $$5 from "./islands/KMenu/commands/create_article.ts";
|
||||
import * as $$6 from "./islands/KMenu/commands/create_movie.ts";
|
||||
import * as $$7 from "./islands/KMenu/types.ts";
|
||||
|
||||
const manifest = {
|
||||
routes: {
|
||||
@ -66,7 +69,10 @@ const manifest = {
|
||||
"./islands/IngredientsList.tsx": $$1,
|
||||
"./islands/KMenu.tsx": $$2,
|
||||
"./islands/KMenu/commands.ts": $$3,
|
||||
"./islands/KMenu/types.ts": $$4,
|
||||
"./islands/KMenu/commands/add_movie_infos.ts": $$4,
|
||||
"./islands/KMenu/commands/create_article.ts": $$5,
|
||||
"./islands/KMenu/commands/create_movie.ts": $$6,
|
||||
"./islands/KMenu/types.ts": $$7,
|
||||
},
|
||||
baseUrl: import.meta.url,
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ export const KMenu = (
|
||||
const loadingText = useSignal("");
|
||||
const activeIndex = useSignal(-1);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const commandInput = useSignal("");
|
||||
|
||||
@ -83,6 +84,23 @@ export const KMenu = (
|
||||
}, context);
|
||||
}
|
||||
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
const selectedItem = container.children[activeIndex.value];
|
||||
if (selectedItem) {
|
||||
const itemPosition = selectedItem.getBoundingClientRect();
|
||||
const containerPosition = container.getBoundingClientRect();
|
||||
|
||||
// Check if the selected item is above the visible area
|
||||
if (itemPosition.top < containerPosition.top) {
|
||||
container.scrollTop -= containerPosition.top - itemPosition.top;
|
||||
} // Check if the selected item is below the visible area
|
||||
else if (itemPosition.bottom > containerPosition.bottom) {
|
||||
container.scrollTop += itemPosition.bottom - containerPosition.bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener("keydown", (ev: KeyboardEvent) => {
|
||||
if (ev.key === "/" && ev.ctrlKey) {
|
||||
visible.value = !visible.value;
|
||||
@ -140,7 +158,7 @@ export const KMenu = (
|
||||
} border-gray-500 `}
|
||||
style={{
|
||||
gridTemplateColumns: activeState.value !== "loading"
|
||||
? "4em 1fr"
|
||||
? "auto 1fr"
|
||||
: "1fr",
|
||||
}}
|
||||
>
|
||||
@ -148,7 +166,7 @@ export const KMenu = (
|
||||
(
|
||||
<>
|
||||
<div class="grid place-items-center border-r border-gray-500">
|
||||
<span class="text-white">
|
||||
<span class="text-white mx-4">
|
||||
{activeMenu.title}
|
||||
</span>
|
||||
</div>
|
||||
@ -172,7 +190,11 @@ export const KMenu = (
|
||||
</div>
|
||||
{activeState.value === "normal" &&
|
||||
(
|
||||
<div class="" style={{ maxHeight: "12rem", overflowY: "auto" }}>
|
||||
<div
|
||||
class=""
|
||||
style={{ maxHeight: "12rem", overflowY: "auto" }}
|
||||
ref={containerRef}
|
||||
>
|
||||
{entries?.length === 0 && (
|
||||
<div class="text-gray-400 px-4 py-2">
|
||||
No Entries
|
||||
|
@ -1,56 +1,12 @@
|
||||
import { Menu } from "@islands/KMenu/types.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { TMDBMovie } from "@lib/types.ts";
|
||||
import { fetchStream, isValidUrl } from "@lib/helpers.ts";
|
||||
import { addMovieInfos } from "@islands/KMenu/commands/add_movie_infos.ts";
|
||||
import { createNewMovie } from "@islands/KMenu/commands/create_movie.ts";
|
||||
import { createNewArticle } from "@islands/KMenu/commands/create_article.ts";
|
||||
|
||||
export const menus: Record<string, Menu> = {
|
||||
main: {
|
||||
title: "Run",
|
||||
entries: [
|
||||
{
|
||||
title: "Close menu",
|
||||
meta: "",
|
||||
cb: (state) => {
|
||||
state.visible.value = false;
|
||||
},
|
||||
visible: () => false,
|
||||
},
|
||||
{
|
||||
title: "Create new article",
|
||||
meta: "",
|
||||
icon: "IconSquareRoundedPlus",
|
||||
cb: (state) => {
|
||||
state.menus["input_link"] = {
|
||||
title: "Link:",
|
||||
entries: [],
|
||||
};
|
||||
|
||||
state.activeMenu.value = "input_link";
|
||||
state.activeState.value = "input";
|
||||
|
||||
const unsub = state.commandInput.subscribe((value) => {
|
||||
if (isValidUrl(value)) {
|
||||
unsub();
|
||||
|
||||
state.activeState.value = "loading";
|
||||
|
||||
fetchStream("/api/articles/create?url=" + value, (chunk) => {
|
||||
console.log({ chunk: chunk.split("\n") });
|
||||
if (chunk.startsWith("id:")) {
|
||||
state.loadingText.value = "Finished";
|
||||
setTimeout(() => {
|
||||
window.location.href = "/articles/" +
|
||||
chunk.replace("id:", "").trim();
|
||||
}, 500);
|
||||
} else {
|
||||
state.loadingText.value = chunk;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
visible: () => true,
|
||||
},
|
||||
{
|
||||
title: "Clear Cache",
|
||||
icon: "IconRefresh",
|
||||
@ -63,58 +19,9 @@ export const menus: Record<string, Menu> = {
|
||||
state.visible.value = false;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Add Movie infos",
|
||||
meta: "",
|
||||
icon: "IconReportSearch",
|
||||
cb: async (state, context) => {
|
||||
state.activeState.value = "loading";
|
||||
const movie = context as Movie;
|
||||
|
||||
const query = movie.name;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/tmdb/query?q=${encodeURIComponent(query)}`,
|
||||
);
|
||||
|
||||
const json = await response.json() as TMDBMovie[];
|
||||
|
||||
const menuID = `result/${movie.name}`;
|
||||
|
||||
state.menus[menuID] = {
|
||||
title: "Select",
|
||||
entries: json.map((m) => ({
|
||||
title: `${m.title} released ${m.release_date}`,
|
||||
cb: async () => {
|
||||
state.activeState.value = "loading";
|
||||
await fetch(`/api/movies/enhance/${movie.name}/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ tmdbId: m.id }),
|
||||
});
|
||||
state.visible.value = false;
|
||||
state.activeState.value = "normal";
|
||||
window.location.reload();
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
state.activeMenu.value = menuID;
|
||||
|
||||
state.activeState.value = "normal";
|
||||
},
|
||||
visible: () => {
|
||||
const loc = globalThis["location"];
|
||||
return loc?.pathname?.includes("movie");
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Reload Page",
|
||||
meta: "",
|
||||
cb: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
visible: () => false,
|
||||
},
|
||||
createNewArticle,
|
||||
createNewMovie,
|
||||
addMovieInfos,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
48
islands/KMenu/commands/add_movie_infos.ts
Normal file
48
islands/KMenu/commands/add_movie_infos.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { TMDBMovie } from "@lib/types.ts";
|
||||
|
||||
export const addMovieInfos: MenuEntry = {
|
||||
title: "Add Movie infos",
|
||||
meta: "",
|
||||
icon: "IconReportSearch",
|
||||
cb: async (state, context) => {
|
||||
state.activeState.value = "loading";
|
||||
const movie = context as Movie;
|
||||
|
||||
const query = movie.name;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/tmdb/query?q=${encodeURIComponent(query)}`,
|
||||
);
|
||||
|
||||
const json = await response.json() as TMDBMovie[];
|
||||
|
||||
const menuID = `result/${movie.name}`;
|
||||
|
||||
state.menus[menuID] = {
|
||||
title: "Select",
|
||||
entries: json.map((m) => ({
|
||||
title: `${m.title} released ${m.release_date}`,
|
||||
cb: async () => {
|
||||
state.activeState.value = "loading";
|
||||
await fetch(`/api/movies/enhance/${movie.name}/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ tmdbId: m.id }),
|
||||
});
|
||||
state.visible.value = false;
|
||||
state.activeState.value = "normal";
|
||||
window.location.reload();
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
state.activeMenu.value = menuID;
|
||||
|
||||
state.activeState.value = "normal";
|
||||
},
|
||||
visible: () => {
|
||||
const loc = globalThis["location"];
|
||||
return loc?.pathname?.includes("movie");
|
||||
},
|
||||
};
|
39
islands/KMenu/commands/create_article.ts
Normal file
39
islands/KMenu/commands/create_article.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { fetchStream, isValidUrl } from "@lib/helpers.ts";
|
||||
|
||||
export const createNewArticle: MenuEntry = {
|
||||
title: "Create new article",
|
||||
meta: "",
|
||||
icon: "IconSquareRoundedPlus",
|
||||
cb: (state) => {
|
||||
state.menus["input_link"] = {
|
||||
title: "Link:",
|
||||
entries: [],
|
||||
};
|
||||
|
||||
state.activeMenu.value = "input_link";
|
||||
state.activeState.value = "input";
|
||||
|
||||
const unsub = state.commandInput.subscribe((value) => {
|
||||
if (isValidUrl(value)) {
|
||||
unsub();
|
||||
|
||||
state.activeState.value = "loading";
|
||||
|
||||
fetchStream("/api/articles/create?url=" + value, (chunk) => {
|
||||
console.log({ chunk: chunk.split("\n") });
|
||||
if (chunk.startsWith("id:")) {
|
||||
state.loadingText.value = "Finished";
|
||||
setTimeout(() => {
|
||||
window.location.href = "/articles/" +
|
||||
chunk.replace("id:", "").trim();
|
||||
}, 500);
|
||||
} else {
|
||||
state.loadingText.value = chunk;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
visible: () => true,
|
||||
};
|
72
islands/KMenu/commands/create_movie.ts
Normal file
72
islands/KMenu/commands/create_movie.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { TMDBMovie } from "@lib/types.ts";
|
||||
import { debounce } from "@lib/helpers.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
|
||||
export const createNewMovie: MenuEntry = {
|
||||
title: "Create new movie",
|
||||
meta: "",
|
||||
icon: "IconSquareRoundedPlus",
|
||||
cb: (state) => {
|
||||
state.menus["input_link"] = {
|
||||
title: "Search",
|
||||
entries: [],
|
||||
};
|
||||
|
||||
state.menus["loading"] = {
|
||||
title: "Search",
|
||||
entries: [
|
||||
{
|
||||
title: "Loading",
|
||||
icon: "IconLoader2",
|
||||
cb() {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
state.activeMenu.value = "input_link";
|
||||
state.activeState.value = "normal";
|
||||
|
||||
let currentQuery: string;
|
||||
const search = debounce(async function search(query: string) {
|
||||
currentQuery = query;
|
||||
console.log({ query });
|
||||
if (query.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/tmdb/query?q=" + query);
|
||||
|
||||
const movies = await response.json() as TMDBMovie[];
|
||||
|
||||
console.log({ query, currentQuery, movies });
|
||||
|
||||
if (query !== currentQuery) return;
|
||||
|
||||
state.menus["input_link"] = {
|
||||
title: "Search",
|
||||
entries: movies.map((r) => {
|
||||
return {
|
||||
title: `${r.title} - ${r.release_date}`,
|
||||
cb: async () => {
|
||||
state.activeState.value = "loading";
|
||||
const response = await fetch("/api/movies/" + r.id, {
|
||||
method: "POST",
|
||||
});
|
||||
const movie = await response.json() as Movie;
|
||||
window.location.href = "/movies/" + movie.name;
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
state.activeMenu.value = "input_link";
|
||||
}, 500);
|
||||
|
||||
const unsub = state.commandInput.subscribe((value) => {
|
||||
state.activeMenu.value = "loading";
|
||||
search(value);
|
||||
});
|
||||
},
|
||||
visible: () => true,
|
||||
};
|
24
lib/cache/cache.ts
vendored
24
lib/cache/cache.ts
vendored
@ -1,4 +1,5 @@
|
||||
import {
|
||||
Bulk,
|
||||
connect,
|
||||
Redis,
|
||||
RedisConnectOptions,
|
||||
@ -14,16 +15,31 @@ async function createCache<T>(): Promise<Map<string, T> | Redis> {
|
||||
const conf: RedisConnectOptions = {
|
||||
hostname: REDIS_HOST,
|
||||
port: REDIS_PORT || 6379,
|
||||
maxRetryCount: 2,
|
||||
};
|
||||
if (REDIS_PASS) {
|
||||
conf.password = REDIS_PASS;
|
||||
}
|
||||
const client = await connect(conf);
|
||||
console.log("[redis] connected");
|
||||
return client;
|
||||
try {
|
||||
const client = await connect(conf);
|
||||
console.log("[redis] connected");
|
||||
return client;
|
||||
} catch (_err) {
|
||||
console.log("[cache] cant connect to redis, falling back to mock");
|
||||
}
|
||||
}
|
||||
|
||||
return new Map<string, T>();
|
||||
const mockRedis = new Map<string, RedisValue>();
|
||||
|
||||
return {
|
||||
async set(key: string, value: RedisValue) {
|
||||
mockRedis.set(key, value);
|
||||
return value.toString();
|
||||
},
|
||||
async get(key: string) {
|
||||
return mockRedis.get(key) as Bulk;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const cache = await createCache();
|
||||
|
@ -76,3 +76,15 @@ export const createStreamResponse = () => {
|
||||
enqueue,
|
||||
};
|
||||
};
|
||||
|
||||
export function debounce<T extends (...args: Parameters<T>) => void>(
|
||||
this: ThisParameterType<T>,
|
||||
fn: T,
|
||||
delay = 300,
|
||||
) {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { parseDocument, renderMarkdown } from "@lib/documents.ts";
|
||||
import { parse } from "yaml";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { createCrud } from "@lib/crud.ts";
|
||||
import { extractHashTags } from "@lib/string.ts";
|
||||
import { extractHashTags, formatDate } from "@lib/string.ts";
|
||||
import { fixRenderedMarkdown } from "@lib/helpers.ts";
|
||||
|
||||
export type Movie = {
|
||||
id: string;
|
||||
@ -18,6 +19,27 @@ export type Movie = {
|
||||
};
|
||||
};
|
||||
|
||||
function renderMovie(movie: Movie) {
|
||||
const meta = movie.meta;
|
||||
if ("date" in meta) {
|
||||
meta.date = formatDate(meta.date);
|
||||
}
|
||||
|
||||
return fixRenderedMarkdown(`${
|
||||
meta
|
||||
? `---
|
||||
${stringify(meta)}
|
||||
---`
|
||||
: `---
|
||||
---`
|
||||
}
|
||||
# ${movie.name}
|
||||
${movie.meta.image ? `` : ""}
|
||||
${movie.tags.map((t) => `#${t}`).join(" ")}
|
||||
${movie.description}
|
||||
`);
|
||||
}
|
||||
|
||||
export function parseMovie(original: string, id: string): Movie {
|
||||
const doc = parseDocument(original);
|
||||
|
||||
@ -80,3 +102,8 @@ const crud = createCrud<Movie>({
|
||||
|
||||
export const getMovie = crud.read;
|
||||
export const getAllMovies = crud.readAll;
|
||||
export const createMovie = (movie: Movie) => {
|
||||
console.log("creating movie", { movie });
|
||||
const content = renderMovie(movie);
|
||||
return crud.create(movie.id, content);
|
||||
};
|
||||
|
@ -1,10 +1,62 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { getMovie } from "@lib/resource/movies.ts";
|
||||
import { createMovie, getMovie, Movie } from "@lib/resource/movies.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import * as tmdb from "@lib/tmdb.ts";
|
||||
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
||||
import { safeFileName } from "@lib/string.ts";
|
||||
import { createDocument } from "@lib/documents.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_, ctx) {
|
||||
const movie = await getMovie(ctx.params.name);
|
||||
return json(movie);
|
||||
},
|
||||
async POST(_, ctx) {
|
||||
const tmdbId = parseInt(ctx.params.name);
|
||||
|
||||
const movieDetails = await tmdb.getMovie(tmdbId);
|
||||
const movieCredits = await tmdb.getMovieCredits(tmdbId);
|
||||
|
||||
const releaseDate = movieDetails.release_date;
|
||||
const posterPath = movieDetails.poster_path;
|
||||
const director =
|
||||
movieCredits?.crew?.filter?.((person) => person.job === "Director")[0];
|
||||
|
||||
let finalPath = "";
|
||||
const name = movieDetails.title || movieDetails.original_title ||
|
||||
ctx.params.name;
|
||||
if (posterPath) {
|
||||
const poster = await tmdb.getMoviePoster(posterPath);
|
||||
const extension = fileExtension(posterPath);
|
||||
|
||||
finalPath = `Media/movies/images/${
|
||||
safeFileName(name)
|
||||
}_cover.${extension}`;
|
||||
await createDocument(finalPath, poster);
|
||||
}
|
||||
|
||||
const metadata = {} as Movie["meta"];
|
||||
if (releaseDate) {
|
||||
metadata.date = new Date(releaseDate);
|
||||
}
|
||||
if (finalPath) {
|
||||
metadata.image = finalPath;
|
||||
}
|
||||
if (director) {
|
||||
metadata.author = director.name;
|
||||
}
|
||||
|
||||
const movie: Movie = {
|
||||
id: name,
|
||||
name: name,
|
||||
type: "movie",
|
||||
description: "",
|
||||
tags: [],
|
||||
meta: metadata,
|
||||
};
|
||||
|
||||
await createMovie(movie);
|
||||
|
||||
return json(movie);
|
||||
},
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ export default function Greet(props: PageProps<Movie>) {
|
||||
|
||||
const { author = "", date = "" } = movie.meta;
|
||||
|
||||
console.log(movie.description)
|
||||
console.log(movie.description);
|
||||
|
||||
return (
|
||||
<MainLayout url={props.url}>
|
||||
@ -35,7 +35,9 @@ export default function Greet(props: PageProps<Movie>) {
|
||||
</>
|
||||
)}
|
||||
<div class="px-8 text-white mt-10">
|
||||
{movie?.description?.length > 80 ? <h2 class="text-4xl font-bold mb-4">Review</h2>:<></>}
|
||||
{movie?.description?.length > 80
|
||||
? <h2 class="text-4xl font-bold mb-4">Review</h2>
|
||||
: <></>}
|
||||
<pre
|
||||
class="whitespace-break-spaces"
|
||||
dangerouslySetInnerHTML={{ __html: movie.description || "" }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user