import { type Accessor, createMemo, createSignal, For, type JSX, onCleanup, onMount, Show, untrack, } from "solid-js"; import type { GalleryFavoriteOption, MyTagMode, } from "../../eh"; import { sharedApply, type GalleryInfoDom, type GalleryInfoTagGroup } from "../../eh"; import texts from "../../i18n"; import { state } from "../../state"; import { refreshMyTags } from "../Enhance/MyTags"; import { WelcomeIcon } from "../WelcomeIcon"; import { Dialog } from "../Widgets/Dialog"; import { DomNode, DomNodes } from "../Widgets/ExternalDom"; import { Icon } from "../Widgets/Icon"; const RATING_STAR_INDEXES = [0, 1, 2, 3, 4]; const RATING_ACTION_BUTTON_CLASS = "flex w-full min-h-[var(--ui-control-size-md)] items-center justify-center ui-py-xs ui-px-md ui-rounded-md border cursor-pointer font-inherit text-center textsize-md font-700 leading-[1.1] transition-[filter,transform,box-shadow] duration-120 active:scale-98 disabled:opacity-50 disabled:cursor-default"; const GALLERY_FAVORITE_ROW_CLASS = "flex box-border w-full ui-hit-min-h-md items-center ui-gap-md ui-py-xs ui-px-lg border-0 border-b ehp-color-site-border-subtle-b bg-transparent ehp-color-site-text font-inherit textsize-md leading-[1.2] text-left"; const GALLERY_FAVORITE_ICON_SIZE = "var(--ui-icon-size-lg)"; const TAG_NAMESPACE_PREFIXES: Readonly> = { artist: "a", character: "c", cosplayer: "cos", female: "f", group: "g", language: "l", location: "loc", male: "m", mixed: "x", other: "o", parody: "p", reclass: "r", }; function exactTagQuery(name: string): string { const separator = name.indexOf(":"); if (separator < 0) { return `tag:${name}$`; } const namespace = name.slice(0, separator).toLowerCase(); const prefix = TAG_NAMESPACE_PREFIXES[namespace] ?? namespace; return `${prefix}:${name.slice(separator + 1)}$`; } type GalleryPanelTagGroup = GalleryInfoTagGroup; export function GalleryInfoPanel(props: { leftHandedControls: Accessor; primaryAction?: JSX.Element; source: GalleryInfoDom; }) { const source = untrack(() => props.source); const rating = source.data.rating; const hasCover = source.elems.cover !== null; const [ratingValue, setRatingValue] = createSignal(rating?.value ?? 0); const [ratingPreview, setRatingPreview] = createSignal(null); const [ratingPickerOpen, setRatingPickerOpen] = createSignal(false); const [ratingSubmitted, setRatingSubmitted] = createSignal( rating?.rated ?? false, ); const [ratingCount] = createSignal(rating?.count ?? ""); const [ratingValueLabel] = createSignal(rating?.label ?? ""); const initialTagGroups = source.data.tagGroups.map((group) => ({ ...group, tags: group.tags.flatMap(({ contentSourceIndex, ...tag }) => { const contentSource = source.elems.tagContents[contentSourceIndex]; return contentSource ? [{ ...tag, contentSource }] : []; }), })); const [tagGroups, setTagGroups] = createSignal(initialTagGroups); const [selectedTag, setSelectedTag] = createSignal< GalleryPanelTagGroup["tags"][number] | null >(null); const [tagging, setTagging] = createSignal(false); let ratingPointerType = ""; const hasNewTag = () => source.elems.newTag !== null; const displayedRating = createMemo(() => ratingPreview() ?? ratingValue()); const closeRatingPicker = () => { setRatingPreview(null); setRatingPickerOpen(false); }; const ratingLabel = createMemo(() => { const preview = ratingPreview(); if (preview !== null) { return `Rate as ${preview.toFixed(1)} stars`; } return ratingSubmitted() ? `Rated ${ratingValue().toFixed(1)} stars` : ratingValueLabel(); }); const previewRatingFromPointer = (event: PointerEvent): void => { if (event.pointerType !== "mouse") { return; } setRatingPreview( ratingFromPointer( event.clientX, event.currentTarget as HTMLElement, ), ); }; onMount(() => { const stopObservingTags = source.handle.observeGalleryTagGroups(setTagGroups); onCleanup(stopObservingTags); }); const submitRating = (value: number): boolean => { if (!rating) { return false; } try { source.handle.submitGalleryRating(value); setRatingValue(value); setRatingPreview(null); setRatingSubmitted(true); return true; } catch (error) { setRatingPreview(null); console.error("[ehpeek]", error); window.alert( error instanceof Error ? error.message : texts.errors.loadFailed, ); return false; } }; const openTagMenu = (tag: GalleryPanelTagGroup["tags"][number]) => { try { source.handle.openGalleryTagMenu(tag); setSelectedTag(tag); } catch (error) { console.error("[ehpeek] Gallery tag actions failed", error); window.alert( error instanceof Error ? error.message : texts.errors.loadFailed, ); } }; const closeTagMenu = () => { if (!selectedTag()) { return; } source.handle.closeGalleryTagMenu(); setSelectedTag(null); }; const updateTag = (updatedTag: GalleryPanelTagGroup["tags"][number]) => { setTagGroups((groups) => groups.map((group) => ({ ...group, tags: group.tags.map((tag) => tag.url === updatedTag.url ? updatedTag : tag), }))); setSelectedTag((tag) => tag?.url === updatedTag.url ? updatedTag : tag); }; return (
{(item) => (
{item.value}
)}
{(tagGroups().length > 0 || hasNewTag()) && (
event.preventDefault()} > 0}>
{(group) => ( )}
)}
{ratingLabel()}
); } function TouchGalleryActionsMenu(props: { items: GalleryInfoDom["elems"]["actionItems"]; }) { const [open, setOpen] = createSignal(false); let root!: HTMLDivElement; onMount(() => { const onClick = (event: MouseEvent) => { if (event.target instanceof Element && root.contains(event.target)) { return; } setOpen(false); }; document.addEventListener("click", onClick); onCleanup(() => { document.removeEventListener("click", onClick); }); }); return (
); } function TouchGalleryTagGroup(props: { group: GalleryPanelTagGroup; onTagOpen: (tag: GalleryPanelTagGroup["tags"][number]) => void; tagging: boolean; }) { return (
{props.group.namespace}
{ if ( !props.tagging || event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ) { return; } const link = event.target instanceof Element ? event.target.closest("a.ehpeek-touch-gallery-tag") : null; const href = link?.getAttribute("href"); const tag = props.group.tags.find((candidate) => candidate.url === href); if (!tag) { return; } event.preventDefault(); props.onTagOpen(tag); }} > {(tag) => ( )}
); } function TouchGalleryTag(props: { tag: GalleryPanelTagGroup["tags"][number]; }) { return ( ); } function TouchGalleryTagMenu(props: { source: GalleryInfoDom; tag: GalleryPanelTagGroup["tags"][number] | null; onClose: () => void; onTagUpdated: (tag: GalleryPanelTagGroup["tags"][number]) => void; }) { const onClose = untrack(() => props.onClose); const onTagUpdated = untrack(() => props.onTagUpdated); const [favoriteDialogOpen, setFavoriteDialogOpen] = createSignal(false); const tagSets = state.gallery.myTagSets.reload(); const [selectedTagSet, setSelectedTagSet] = createSignal( tagSets.find((option) => option.selected)?.value ?? tagSets[0]?.value ?? "1", ); const [collectionOpen, setCollectionOpen] = createSignal(false); const [tagMode, setTagMode] = createSignal("marked"); const [updating, setUpdating] = createSignal(false); const [favoriteTag, setFavoriteTag] = createSignal< GalleryPanelTagGroup["tags"][number] | null >(null); const closeFavoriteTagDialog = () => { setCollectionOpen(false); setFavoriteDialogOpen(false); }; onMount(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape" && props.tag) { props.onClose(); } }; document.addEventListener("keydown", onKeyDown); onCleanup(() => { document.removeEventListener("keydown", onKeyDown); }); }); const updateFavoriteTag = async (tag: GalleryPanelTagGroup["tags"][number]) => { if (updating()) { return; } setUpdating(true); try { const myTagsPage = tag.myTag ? await props.source.handle.removeFavoriteTag(tag) : await props.source.handle.submitFavoriteTag(tag, selectedTagSet(), tagMode()); const updateAppearance = ( appearance: typeof myTagsPage.appearances[number] | undefined, ) => onTagUpdated({ ...tag, appearance: appearance ? { ...tag.appearance, backgroundColor: appearance.backgroundColor, color: appearance.color, } : { backgroundColor: "", borderColor: "", color: "" }, myTag: appearance ? { id: appearance.id, tagSet: appearance.tagSet } : null, }); updateAppearance(myTagsPage.appearances.find((item) => item.name === tag.name)); setFavoriteDialogOpen(false); onClose(); void refreshMyTags(myTagsPage).then((appearances) => { if (appearances) { updateAppearance(appearances.find((item) => item.name === tag.name)); } }); } catch (error) { console.error("[ehpeek]", error); window.alert( error instanceof Error ? error.message : texts.errors.loadFailed, ); } finally { setUpdating(false); } }; return ( <> } >
{texts.gallery.tagCollection}
{(option) => ( )}
{texts.gallery.tagBehavior}
{([value, label]) => ( )}
); } function TouchGalleryNewTag(props: { source: GalleryInfoDom }) { return ; } function TouchGalleryTagContent(props: { tag: GalleryPanelTagGroup["tags"][number]; }) { let host!: HTMLSpanElement; onMount(() => { onCleanup(props.tag.contentSource.mirrorContentTo(host)); }); return ( ); } function TouchGalleryFavoriteButton(props: { source: GalleryInfoDom }) { const [favorite, setFavorite] = createSignal( untrack(() => ({ ...props.source.data.favorite })), ); const [open, setOpen] = createSignal(false); const [loadingState, setLoadingState] = createSignal< "idle" | "loading" | "failed" >("idle"); const [options, setOptions] = createSignal([]); const [note, setNote] = createSignal(""); const [noteDraft, setNoteDraft] = createSignal(""); const [editingNote, setEditingNote] = createSignal(false); const favorited = () => favorite().favorited; const closeMenu = () => { if ( noteDraft() !== note() && !window.confirm(texts.gallery.discardFavoriteNote) ) { return; } setNoteDraft(note()); setEditingNote(false); setOpen(false); }; const openMenu = async () => { const currentFavorite = favorite(); if (!currentFavorite.actionUrl) { return; } setOpen(true); setEditingNote(false); setNote(""); setNoteDraft(""); setLoadingState("loading"); try { const dialog = await props.source.handle.loadGalleryFavoriteDialog( currentFavorite.actionUrl, currentFavorite.favorited, ); setOptions(dialog.options); setNote(dialog.note); setNoteDraft(dialog.note); setLoadingState("idle"); } catch (error) { console.error("[ehpeek]", error); setLoadingState("failed"); } }; const updateFavorite = async (option: GalleryFavoriteOption) => { const actionUrl = favorite().actionUrl; if (!actionUrl || loadingState() === "loading") { return; } setLoadingState("loading"); try { await props.source.handle.updateGalleryFavorite( actionUrl, option.value, noteDraft(), ); setFavorite({ ...favorite(), color: option.color, favorited: option.value !== "favdel", label: option.value === "favdel" ? texts.gallery.notFavorited : option.label, }); setNote(noteDraft()); setLoadingState("idle"); setOpen(false); } catch (error) { console.error("[ehpeek]", error); setLoadingState("failed"); } }; return (
option.value !== "favdel")}> {(option) => ( void updateFavorite(option)} /> )} option.value === "favdel")}> {(option) => ( void updateFavorite(option)} /> )} } >