diff --git a/src/web/src/components/HelpCenter/HelpCenterButton.tsx b/src/web/src/components/HelpCenter/HelpCenterButton.tsx new file mode 100644 index 000000000..acf13150b --- /dev/null +++ b/src/web/src/components/HelpCenter/HelpCenterButton.tsx @@ -0,0 +1,78 @@ +"use client"; + +import type { HelpTarget } from "./types"; + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AiOutlineQuestionCircle } from "react-icons/ai"; + +import HelpCenterModal from "./HelpCenterModal"; + +import { Button, Tooltip } from "@/components/bakaui"; + +export interface HelpCenterButtonProps extends HelpTarget { + /** When set, renders a labelled button instead of the icon-only "?" button. */ + label?: string; + size?: "sm" | "md"; + className?: string; +} + +/** + * The "?" entry point. Drop it next to any UI that involves a help center + * topic; it opens the help center at the given topic/section. + */ +const HelpCenterButton = ({ + topic, + section, + concept, + label, + size = "sm", + className, +}: HelpCenterButtonProps) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + + const button = label ? ( + + ) : ( + + + + ); + + return ( + <> + {button} + {visible && ( + setVisible(false)} + /> + )} + + ); +}; + +HelpCenterButton.displayName = "HelpCenterButton"; + +export default HelpCenterButton; diff --git a/src/web/src/components/HelpCenter/HelpCenterModal.tsx b/src/web/src/components/HelpCenter/HelpCenterModal.tsx new file mode 100644 index 000000000..740824c62 --- /dev/null +++ b/src/web/src/components/HelpCenter/HelpCenterModal.tsx @@ -0,0 +1,144 @@ +"use client"; + +import type { HelpTarget, HelpTopicId } from "./types"; + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AiOutlineQuestionCircle } from "react-icons/ai"; + +import { helpTopics } from "./topics"; + +import { Button, Modal } from "@/components/bakaui"; + +export interface HelpCenterModalProps extends HelpTarget { + visible: boolean; + onClose: () => void; + /** First-run mode: opened automatically for new users, closes via a single primary action. */ + firstRun?: boolean; +} + +interface ActiveEntry { + topicId: HelpTopicId; + /** Undefined = the topic's overview entry is selected. */ + conceptId?: string; +} + +/** + * The help center host: a left navigation of topics and their concepts, and a + * right pane with the selected entry's content. Content is rendered by topic + * components so the same content can later be hosted by a standalone page. + */ +const HelpCenterModal = ({ + visible, + onClose, + topic, + section, + concept, + firstRun, +}: HelpCenterModalProps) => { + const { t } = useTranslation(); + const initialTopicId = topic ?? helpTopics[0]!.id; + const [active, setActive] = useState({ + topicId: initialTopicId, + conceptId: concept, + }); + + const activeTopic = helpTopics.find((item) => item.id === active.topicId) ?? helpTopics[0]!; + const { Content, ConceptContent } = activeTopic; + + return ( + + + + ) : ( + false + ) + } + isDismissable={!firstRun} + size="6xl" + title={ +
+ + {t("helpCenter.title")} +
+ } + visible={visible} + onClose={onClose} + > +
+ {/* Left navigation: one overview entry per topic + its concept entries */} +
+ {helpTopics.map((topicDef) => { + const isOverviewActive = + topicDef.id === active.topicId && active.conceptId == undefined; + + return ( +
+ + + {topicDef.concepts && topicDef.concepts.length > 0 && ( + <> + {topicDef.conceptGroupLabelKey && ( +
+ {t(topicDef.conceptGroupLabelKey)} +
+ )} + {topicDef.concepts.map((item) => { + const isActive = + topicDef.id === active.topicId && active.conceptId === item.id; + + return ( + + ); + })} + + )} +
+ ); + })} +
+ + {/* Right pane */} +
+ {active.conceptId != undefined && ConceptContent ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +HelpCenterModal.displayName = "HelpCenterModal"; + +export default HelpCenterModal; diff --git a/src/web/src/components/HelpCenter/index.ts b/src/web/src/components/HelpCenter/index.ts new file mode 100644 index 000000000..84f2cbde3 --- /dev/null +++ b/src/web/src/components/HelpCenter/index.ts @@ -0,0 +1,4 @@ +export { default as HelpCenterButton } from "./HelpCenterButton"; +export { default as HelpCenterModal } from "./HelpCenterModal"; +export { useFirstRunHelp, PATH_MARK_FIRST_RUN_KEY } from "./useFirstRunHelp"; +export type { HelpTarget, HelpTopicId, HelpSectionId } from "./types"; diff --git a/src/web/src/components/HelpCenter/topics.tsx b/src/web/src/components/HelpCenter/topics.tsx new file mode 100644 index 000000000..9b6c8f2b7 --- /dev/null +++ b/src/web/src/components/HelpCenter/topics.tsx @@ -0,0 +1,29 @@ +import type { HelpTopicDefinition, HelpTopicId } from "./types"; + +import { AiOutlineTags } from "react-icons/ai"; + +import PathMarkTopic from "./topics/pathMark"; +import PathMarkConceptDetail from "./topics/pathMark/ConceptDetail"; +import { pathMarkConcepts } from "./topics/pathMark/concepts"; + +/** + * Registry of all help center topics. Other guides (onboarding, resource + * profile, file mover, ...) join the help center by adding an entry here. + */ +export const helpTopics: HelpTopicDefinition[] = [ + { + id: "pathMark", + titleKey: "helpCenter.topic.pathMark", + icon: , + Content: PathMarkTopic, + conceptGroupLabelKey: "helpCenter.pathMark.section.concepts", + concepts: pathMarkConcepts.map((concept) => ({ + id: concept.id, + labelKey: `helpCenter.pathMark.concept.${concept.id}.name`, + })), + ConceptContent: PathMarkConceptDetail, + }, +]; + +export const getHelpTopic = (id: HelpTopicId): HelpTopicDefinition => + helpTopics.find((topic) => topic.id === id) ?? helpTopics[0]!; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/ComparisonSection.tsx b/src/web/src/components/HelpCenter/topics/pathMark/ComparisonSection.tsx new file mode 100644 index 000000000..eb280f9ca --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/ComparisonSection.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import { AiOutlineCheck, AiOutlineClose, AiOutlineMinus } from "react-icons/ai"; + +type SupportLevel = "yes" | "partial" | "no"; + +interface ComparisonRow { + id: string; + /** [scraper-style manager, manual tagging tool, Bakabase path marks] */ + levels: [SupportLevel, SupportLevel, SupportLevel]; +} + +const rows: ComparisonRow[] = [ + { id: "noRestructure", levels: ["no", "yes", "yes"] }, + { id: "anyLevel", levels: ["no", "partial", "yes"] }, + { id: "regexMatch", levels: ["no", "no", "yes"] }, + { id: "dynamicProperty", levels: ["partial", "no", "yes"] }, + { id: "dynamicLibrary", levels: ["no", "no", "yes"] }, + { id: "stackedRules", levels: ["no", "partial", "yes"] }, + { id: "preview", levels: ["no", "no", "yes"] }, + { id: "anyFileType", levels: ["partial", "partial", "yes"] }, + { id: "keepDataOnMove", levels: ["partial", "no", "yes"] }, + { id: "portableRules", levels: ["no", "no", "yes"] }, +]; + +const LevelIcon = ({ level }: { level: SupportLevel }) => { + switch (level) { + case "yes": + return ; + case "partial": + return ; + case "no": + return ; + } +}; + +const k = (key: string) => `helpCenter.pathMark.comparison.${key}`; + +const ComparisonSection = () => { + const { t } = useTranslation(); + + return ( +
+

{t(k("intro"))}

+ +
+ + + + + + + + + + + {rows.map((row) => ( + + + + + + + ))} + +
+ {t(k("column.capability"))} + + {t(k("column.scraper"))} + + {t(k("column.manual"))} + + {t(k("column.bakabase"))} +
{t(k(`row.${row.id}`))} + + + + + +
+
+ +
+ + + {t(k("legend.yes"))} + + + + {t(k("legend.partial"))} + + + + {t(k("legend.no"))} + +
+ +

{t(k("note"))}

+
+ ); +}; + +ComparisonSection.displayName = "ComparisonSection"; + +export default ComparisonSection; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/ConceptDetail.tsx b/src/web/src/components/HelpCenter/topics/pathMark/ConceptDetail.tsx new file mode 100644 index 000000000..e58fd0037 --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/ConceptDetail.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useTranslation } from "react-i18next"; + +import { pathMarkConcepts } from "./concepts"; + +/** Detail page of one path mark concept, selected from the left navigation. */ +const ConceptDetail = ({ conceptId }: { conceptId: string }) => { + const { t } = useTranslation(); + const concept = pathMarkConcepts.find((item) => item.id === conceptId); + + if (!concept) { + return null; + } + + const base = `helpCenter.pathMark.concept.${concept.id}`; + + return ( +
+
+

{t(`${base}.name`)}

+

{t(`${base}.short`)}

+
+ +

{t(`${base}.long`)}

+ + {concept.hasExample && ( +
+ {t(`${base}.example`)} +
+ )} +
+ ); +}; + +ConceptDetail.displayName = "ConceptDetail"; + +export default ConceptDetail; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/DirectoryTree.tsx b/src/web/src/components/HelpCenter/topics/pathMark/DirectoryTree.tsx new file mode 100644 index 000000000..584232c2d --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/DirectoryTree.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import { AiOutlineFile, AiOutlineFolder } from "react-icons/ai"; + +export type TreeMarkType = "resource" | "property" | "mediaLibrary"; + +export interface TreeLine { + depth: number; + /** i18n key under helpCenter.pathMark.node.* for localizable folder names. */ + nameKey?: string; + /** Literal name (file names such as movie.mkv that need no translation). */ + literal?: string; + kind: "dir" | "file"; + /** Highlights the line with the mark type's color and shows a badge. */ + mark?: TreeMarkType; + /** i18n key of the badge text; defaults to the mark type name. */ + badgeKey?: string; + /** Dim the line (context-only entries such as "…"). */ + muted?: boolean; +} + +const markStyles: Record = { + resource: { + line: "bg-success/10", + badge: "bg-success/15 text-success", + }, + property: { + line: "bg-primary/10", + badge: "bg-primary/15 text-primary", + }, + mediaLibrary: { + line: "bg-secondary/10", + badge: "bg-secondary/15 text-secondary", + }, +}; + +const defaultBadgeKeys: Record = { + resource: "helpCenter.pathMark.badge.resource", + property: "helpCenter.pathMark.badge.property", + mediaLibrary: "helpCenter.pathMark.badge.mediaLibrary", +}; + +/** + * Renders a sample directory tree with mark highlights. Shared by the + * "what is it" diagram and every example card so trees look identical + * everywhere. + */ +const DirectoryTree = ({ lines, className }: { lines: TreeLine[]; className?: string }) => { + const { t } = useTranslation(); + + return ( +
+ {lines.map((line, index) => { + const style = line.mark ? markStyles[line.mark] : undefined; + const name = line.nameKey ? t(line.nameKey) : (line.literal ?? ""); + const badgeText = line.mark + ? t(line.badgeKey ?? defaultBadgeKeys[line.mark]) + : line.badgeKey + ? t(line.badgeKey) + : undefined; + + return ( +
+ {line.kind === "dir" ? ( + + ) : ( + + )} + {name} + {badgeText && ( + + {badgeText} + + )} +
+ ); + })} +
+ ); +}; + +DirectoryTree.displayName = "DirectoryTree"; + +export default DirectoryTree; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/ExamplesSection.tsx b/src/web/src/components/HelpCenter/topics/pathMark/ExamplesSection.tsx new file mode 100644 index 000000000..eeb8164ea --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/ExamplesSection.tsx @@ -0,0 +1,76 @@ +"use client"; + +import type { TreeMarkType } from "./DirectoryTree"; + +import { useTranslation } from "react-i18next"; + +import DirectoryTree from "./DirectoryTree"; +import { pathMarkExamples } from "./examples"; + +import { Chip } from "@/components/bakaui"; + +const markTypeColors: Record = { + resource: "bg-success", + property: "bg-primary", + mediaLibrary: "bg-secondary", +}; + +const ExamplesSection = () => { + const { t } = useTranslation(); + + return ( +
+

{t("helpCenter.pathMark.examples.intro")}

+ +
+ {pathMarkExamples.map((example) => { + const base = `helpCenter.pathMark.examples.${example.id}`; + + return ( +
+ {/* Title + ability tags */} +
+

{t(`${base}.title`)}

+
+ {example.abilities.map((ability) => ( + + {t(`helpCenter.pathMark.ability.${ability}`)} + + ))} +
+
+ +

{t(`${base}.desc`)}

+ + + + {/* Marks to configure */} +
+ {example.markTypes.map((markType, index) => ( +
+ + {t(`${base}.mark${index + 1}`)} +
+ ))} +
+ + {/* Result */} +
+ {t(`${base}.result`)} +
+
+ ); + })} +
+
+ ); +}; + +ExamplesSection.displayName = "ExamplesSection"; + +export default ExamplesSection; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/WhatIsSection.tsx b/src/web/src/components/HelpCenter/topics/pathMark/WhatIsSection.tsx new file mode 100644 index 000000000..83c5c2b15 --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/WhatIsSection.tsx @@ -0,0 +1,144 @@ +"use client"; + +import type { TreeLine } from "./DirectoryTree"; + +import { useTranslation } from "react-i18next"; +import { AiOutlineArrowRight, AiOutlineFileAdd, AiOutlineTags } from "react-icons/ai"; +import { MdVideoLibrary } from "react-icons/md"; + +import DirectoryTree from "./DirectoryTree"; + +const k = (key: string) => `helpCenter.pathMark.whatIs.${key}`; + +/** The same sample tree as the movieGenre example, kept small for the diagram. */ +const diagramTree: TreeLine[] = [ + { + depth: 0, + kind: "dir", + nameKey: "helpCenter.pathMark.node.movies", + mark: "mediaLibrary", + badgeKey: "helpCenter.pathMark.badge.libraryMovies", + }, + { + depth: 1, + kind: "dir", + nameKey: "helpCenter.pathMark.node.scifi", + mark: "property", + badgeKey: "helpCenter.pathMark.badge.genreDynamic", + }, + { depth: 2, kind: "dir", nameKey: "helpCenter.pathMark.node.interstellar", mark: "resource" }, + { + depth: 1, + kind: "dir", + nameKey: "helpCenter.pathMark.node.drama", + mark: "property", + badgeKey: "helpCenter.pathMark.badge.genreDynamic", + }, + { depth: 2, kind: "dir", nameKey: "helpCenter.pathMark.node.shawshank", mark: "resource" }, +]; + +const markTypeCards = [ + { + id: "resource", + icon: , + color: "success", + style: "bg-success/5 border-success/20 text-success", + }, + { + id: "property", + icon: , + color: "primary", + style: "bg-primary/5 border-primary/20 text-primary", + }, + { + id: "mediaLibrary", + icon: , + color: "secondary", + style: "bg-secondary/5 border-secondary/20 text-secondary", + }, +] as const; + +const WhatIsSection = () => { + const { t } = useTranslation(); + + return ( +
+ {/* Mental model */} +
+

{t(k("headline"))}

+

{t(k("intro"))}

+
+ + {/* Tree -> result diagram */} +
+
+
{t(k("diagram.treeTitle"))}
+ +
+ +
+
{t(k("diagram.resultTitle"))}
+
+
+ + {t(k("diagram.resultResources"))} +
+
+ + {t(k("diagram.resultProperties"))} +
+
+ + {t(k("diagram.resultLibrary"))} +
+
{t(k("diagram.resultNote"))}
+
+
+
+ + {/* Three mark types */} +
+

{t(k("markTypes.title"))}

+
+ {markTypeCards.map((card) => ( +
+
+ {card.icon} + {t(k(`markTypes.${card.id}.name`))} +
+

{t(k(`markTypes.${card.id}.desc`))}

+
+ ))} +
+
+ {t(k("markTypes.resourceIsCore"))} +
+
+ + {/* Workflow */} +
+

{t(k("workflow.title"))}

+
+ {[1, 2, 3].map((step) => ( +
+
+ {step} +
+
+
+ {t(k(`workflow.step${step}.title`))} +
+
{t(k(`workflow.step${step}.desc`))}
+
+
+ ))} +
+

{t(k("workflow.syncNote"))}

+
+
+ ); +}; + +WhatIsSection.displayName = "WhatIsSection"; + +export default WhatIsSection; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/concepts.ts b/src/web/src/components/HelpCenter/topics/pathMark/concepts.ts new file mode 100644 index 000000000..746f3e434 --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/concepts.ts @@ -0,0 +1,23 @@ +export interface PathMarkConcept { + id: string; + /** Show an extra example line (`helpCenter.pathMark.concept.{id}.example`). */ + hasExample?: boolean; +} + +/** + * Glossary of path mark concepts. Text lives under + * `helpCenter.pathMark.concept.{id}.name/.short/.long[/.example]` so the same + * wording can be reused by inline tips later without drifting. + */ +export const pathMarkConcepts: PathMarkConcept[] = [ + { id: "layer", hasExample: true }, + { id: "regex", hasExample: true }, + { id: "matchMode" }, + { id: "applyScope", hasExample: true }, + { id: "dynamicValue", hasExample: true }, + { id: "priority" }, + { id: "sync" }, + { id: "boundary" }, + { id: "scheduledSync" }, + { id: "keepIdentity" }, +]; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/examples.ts b/src/web/src/components/HelpCenter/topics/pathMark/examples.ts new file mode 100644 index 000000000..b86a7f65d --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/examples.ts @@ -0,0 +1,259 @@ +import type { TreeLine, TreeMarkType } from "./DirectoryTree"; + +const node = (key: string) => `helpCenter.pathMark.node.${key}`; +const badge = (key: string) => `helpCenter.pathMark.badge.${key}`; + +export type PathMarkAbility = + | "layer" + | "anyLevel" + | "regex" + | "dynamicProperty" + | "dynamicMediaLibrary" + | "filter" + | "boundary" + | "scope" + | "priority" + | "multiMark" + | "schedule" + | "identity"; + +export interface PathMarkExample { + id: string; + abilities: PathMarkAbility[]; + /** + * Mark types configured in this example, in display order. The i-th entry's + * description is `helpCenter.pathMark.examples.{id}.mark{i+1}`. + */ + markTypes: TreeMarkType[]; + tree: TreeLine[]; +} + +/** + * The example gallery ("recipes"). Each entry demonstrates one or two + * capabilities using regular movie / anime / manga / music collections. + */ +export const pathMarkExamples: PathMarkExample[] = [ + { + id: "movieBasic", + abilities: ["layer"], + markTypes: ["resource", "mediaLibrary"], + tree: [ + { + depth: 0, + kind: "dir", + nameKey: node("movies"), + mark: "mediaLibrary", + badgeKey: badge("libraryMovies"), + }, + { depth: 1, kind: "dir", nameKey: node("interstellar"), mark: "resource" }, + { depth: 2, kind: "file", literal: "movie.mkv", muted: true }, + { depth: 1, kind: "dir", nameKey: node("inception"), mark: "resource" }, + { depth: 2, kind: "file", literal: "movie.mkv", muted: true }, + ], + }, + { + id: "movieGenre", + abilities: ["anyLevel", "dynamicProperty"], + markTypes: ["resource", "property"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("movies") }, + { + depth: 1, + kind: "dir", + nameKey: node("scifi"), + mark: "property", + badgeKey: badge("genreDynamic"), + }, + { depth: 2, kind: "dir", nameKey: node("interstellar"), mark: "resource" }, + { + depth: 1, + kind: "dir", + nameKey: node("drama"), + mark: "property", + badgeKey: badge("genreDynamic"), + }, + { depth: 2, kind: "dir", nameKey: node("shawshank"), mark: "resource" }, + ], + }, + { + id: "animeSeason", + abilities: ["dynamicProperty", "layer"], + markTypes: ["resource", "property"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("anime") }, + { + depth: 1, + kind: "dir", + literal: "2024-04", + mark: "property", + badgeKey: badge("seasonDynamic"), + }, + { depth: 2, kind: "dir", nameKey: node("frieren"), mark: "resource" }, + { depth: 3, kind: "file", nameKey: node("ep01"), muted: true }, + { depth: 3, kind: "file", nameKey: node("ep02"), muted: true }, + ], + }, + { + id: "mangaAuthor", + abilities: ["regex", "dynamicProperty"], + markTypes: ["resource", "property"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("manga") }, + { depth: 1, kind: "dir", nameKey: node("onePiece"), mark: "resource" }, + { depth: 1, kind: "dir", nameKey: node("slamDunk"), mark: "resource" }, + ], + }, + { + id: "musicLibrary", + abilities: ["anyLevel", "dynamicProperty", "filter"], + markTypes: ["resource", "property"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("music") }, + { + depth: 1, + kind: "dir", + nameKey: node("hisaishi"), + mark: "property", + badgeKey: badge("artistDynamic"), + }, + { depth: 2, kind: "dir", nameKey: node("laputaOst"), mark: "resource" }, + { depth: 3, kind: "file", literal: "01.flac", muted: true }, + { depth: 3, kind: "file", literal: "02.flac", muted: true }, + ], + }, + { + id: "autoLibraries", + abilities: ["dynamicMediaLibrary"], + markTypes: ["mediaLibrary", "resource"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("collections") }, + { + depth: 1, + kind: "dir", + nameKey: node("movies"), + mark: "mediaLibrary", + badgeKey: badge("libraryAuto"), + }, + { depth: 2, kind: "dir", literal: "…", muted: true }, + { + depth: 1, + kind: "dir", + nameKey: node("anime"), + mark: "mediaLibrary", + badgeKey: badge("libraryAuto"), + }, + { depth: 2, kind: "dir", literal: "…", muted: true }, + { + depth: 1, + kind: "dir", + nameKey: node("manga"), + mark: "mediaLibrary", + badgeKey: badge("libraryAuto"), + }, + ], + }, + { + id: "multiMarks", + abilities: ["multiMark", "priority"], + markTypes: ["mediaLibrary", "property", "property", "resource"], + tree: [ + { + depth: 0, + kind: "dir", + nameKey: node("anime"), + mark: "mediaLibrary", + badgeKey: badge("libraryAnime"), + }, + { + depth: 1, + kind: "dir", + nameKey: node("ongoing"), + mark: "property", + badgeKey: badge("statusOngoing"), + }, + { + depth: 2, + kind: "dir", + literal: "2024-04", + mark: "property", + badgeKey: badge("seasonDynamic"), + }, + { depth: 3, kind: "dir", nameKey: node("frieren"), mark: "resource" }, + ], + }, + { + id: "extensionFilter", + abilities: ["filter"], + markTypes: ["resource"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("movies") }, + { depth: 1, kind: "dir", nameKey: node("interstellar") }, + { depth: 2, kind: "file", literal: "movie.mkv", mark: "resource" }, + { depth: 2, kind: "file", literal: "movie.srt", muted: true }, + { depth: 2, kind: "file", literal: "poster.jpg", muted: true }, + { depth: 2, kind: "file", literal: "movie.nfo", muted: true }, + ], + }, + { + id: "resourceBoundary", + abilities: ["boundary"], + markTypes: ["resource"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("movies") }, + { + depth: 1, + kind: "dir", + nameKey: node("avatarBd"), + mark: "resource", + badgeKey: badge("boundary"), + }, + { depth: 2, kind: "dir", literal: "BDMV", muted: true }, + { depth: 3, kind: "dir", literal: "STREAM", muted: true }, + { depth: 4, kind: "file", literal: "00001.m2ts", muted: true }, + ], + }, + { + id: "scopeTag", + abilities: ["scope"], + markTypes: ["property"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("manga") }, + { + depth: 1, + kind: "dir", + nameKey: node("completed"), + mark: "property", + badgeKey: badge("statusCompleted"), + }, + { depth: 2, kind: "dir", nameKey: node("slamDunk"), mark: "resource" }, + { depth: 2, kind: "dir", nameKey: node("dragonBall"), mark: "resource" }, + ], + }, + { + id: "scheduledSync", + abilities: ["schedule"], + markTypes: ["resource"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("downloads") }, + { depth: 1, kind: "dir", nameKey: node("frieren"), mark: "resource" }, + { + depth: 1, + kind: "dir", + nameKey: node("newDownload"), + mark: "resource", + badgeKey: badge("autoAdded"), + }, + ], + }, + { + id: "keepIdentity", + abilities: ["identity"], + markTypes: ["resource"], + tree: [ + { depth: 0, kind: "dir", nameKey: node("movies") }, + { depth: 1, kind: "dir", nameKey: node("interstellar"), mark: "resource" }, + { depth: 2, kind: "file", literal: "movie.mkv", muted: true }, + { depth: 2, kind: "file", literal: "bakabase.json", badgeKey: badge("identityFile") }, + ], + }, +]; diff --git a/src/web/src/components/HelpCenter/topics/pathMark/index.tsx b/src/web/src/components/HelpCenter/topics/pathMark/index.tsx new file mode 100644 index 000000000..f17935f7f --- /dev/null +++ b/src/web/src/components/HelpCenter/topics/pathMark/index.tsx @@ -0,0 +1,49 @@ +"use client"; + +import type { HelpTopicContentProps, PathMarkHelpSectionId } from "../../types"; + +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import WhatIsSection from "./WhatIsSection"; +import ExamplesSection from "./ExamplesSection"; +import ComparisonSection from "./ComparisonSection"; + +import { Tab, Tabs } from "@/components/bakaui"; + +const sectionIds: PathMarkHelpSectionId[] = ["whatIs", "examples", "comparison"]; + +const PathMarkTopic = ({ section }: HelpTopicContentProps) => { + const { t } = useTranslation(); + const [activeSection, setActiveSection] = useState(section ?? "whatIs"); + + useEffect(() => { + if (section) { + setActiveSection(section); + } + }, [section]); + + return ( +
+ setActiveSection(key as PathMarkHelpSectionId)} + > + {sectionIds.map((id) => ( + + ))} + + + {activeSection === "whatIs" && } + {activeSection === "examples" && } + {activeSection === "comparison" && } +
+ ); +}; + +PathMarkTopic.displayName = "PathMarkTopic"; + +export default PathMarkTopic; diff --git a/src/web/src/components/HelpCenter/types.ts b/src/web/src/components/HelpCenter/types.ts new file mode 100644 index 000000000..56f7116a7 --- /dev/null +++ b/src/web/src/components/HelpCenter/types.ts @@ -0,0 +1,50 @@ +import type { ComponentType, ReactNode } from "react"; + +/** + * Every guide living in the help center is a "topic". Adding a new guide = + * adding a topic definition to the registry in `topics.tsx`. Each topic + * contributes one overview entry to the left navigation, plus (optionally) + * a group of concept entries rendered below it. + */ +export type HelpTopicId = "pathMark"; + +/** Horizontal tabs inside the path mark overview. Extend as more topics arrive. */ +export type PathMarkHelpSectionId = "whatIs" | "examples" | "comparison"; + +export type HelpSectionId = PathMarkHelpSectionId; + +/** Where a help entry point should land inside the help center. */ +export interface HelpTarget { + topic?: HelpTopicId; + /** Tab to open inside the topic's overview. */ + section?: HelpSectionId; + /** Concept entry (left navigation) to select instead of the overview. */ + concept?: string; +} + +export interface HelpTopicContentProps { + section?: HelpSectionId; + /** True when the help center was opened automatically for a first-time user. */ + firstRun?: boolean; +} + +export interface HelpConceptNavItem { + id: string; + /** i18n key of the concept name shown in the left navigation. */ + labelKey: string; +} + +export interface HelpTopicDefinition { + id: HelpTopicId; + /** i18n key of the topic name shown as its overview entry in the navigation. */ + titleKey: string; + icon: ReactNode; + /** Overview content (rendered when the topic's own entry is selected). */ + Content: ComponentType; + /** i18n key of the group label above the concept entries. */ + conceptGroupLabelKey?: string; + /** Concept entries listed under the topic in the left navigation. */ + concepts?: HelpConceptNavItem[]; + /** Renders the detail of one concept entry. */ + ConceptContent?: ComponentType<{ conceptId: string }>; +} diff --git a/src/web/src/components/HelpCenter/useFirstRunHelp.ts b/src/web/src/components/HelpCenter/useFirstRunHelp.ts new file mode 100644 index 000000000..58b39cc29 --- /dev/null +++ b/src/web/src/components/HelpCenter/useFirstRunHelp.ts @@ -0,0 +1,45 @@ +import { useCallback, useEffect, useState } from "react"; + +/** + * Opens the help center automatically the first time a user visits a screen + * that a topic covers. Completion is remembered per storage key. + */ +export const useFirstRunHelp = (storageKey: string) => { + const [showFirstRun, setShowFirstRun] = useState(false); + + useEffect(() => { + if (typeof window === "undefined" || typeof localStorage === "undefined") { + return; + } + + let completed: string | null = null; + + try { + completed = localStorage.getItem(storageKey); + } catch { + // Storage unavailable — treat as completed to avoid nagging. + completed = "true"; + } + + if (!completed) { + setShowFirstRun(true); + } + }, [storageKey]); + + const completeFirstRun = useCallback(() => { + try { + localStorage.setItem(storageKey, "true"); + } catch { + // Ignore storage failures; the guide simply reappears next time. + } + setShowFirstRun(false); + }, [storageKey]); + + return { + showFirstRun, + completeFirstRun, + }; +}; + +/** Storage key of the path mark first-run guide (kept from the legacy guide tour). */ +export const PATH_MARK_FIRST_RUN_KEY = "bakabase-path-mark-guide-completed"; diff --git a/src/web/src/i18n.ts b/src/web/src/i18n.ts index a9f9137b5..3a21ea26e 100644 --- a/src/web/src/i18n.ts +++ b/src/web/src/i18n.ts @@ -60,6 +60,7 @@ import enPlaylist from "@/locales/en/components/playlist.json"; import enResourceTransfer from "@/locales/en/components/resourceTransfer.json"; import enBakaChat from "@/locales/en/components/bakaChat.json"; import enNotificationCenter from "@/locales/en/components/notificationCenter.json"; +import enHelpCenter from "@/locales/en/components/helpCenter.json"; // New modular imports - Chinese import cnCommon from "@/locales/cn/common.json"; @@ -120,6 +121,7 @@ import cnPlaylist from "@/locales/cn/components/playlist.json"; import cnResourceTransfer from "@/locales/cn/components/resourceTransfer.json"; import cnBakaChat from "@/locales/cn/components/bakaChat.json"; import cnNotificationCenter from "@/locales/cn/components/notificationCenter.json"; +import cnHelpCenter from "@/locales/cn/components/helpCenter.json"; // Merge all English resources const enResources = { @@ -179,6 +181,7 @@ const enResources = { ...enResourceTransfer, ...enBakaChat, ...enNotificationCenter, + ...enHelpCenter, }; // Merge all Chinese resources @@ -239,6 +242,7 @@ const cnResources = { ...cnResourceTransfer, ...cnBakaChat, ...cnNotificationCenter, + ...cnHelpCenter, }; // 只初始化一次,防止热更新或多次 import 时重复初始化 diff --git a/src/web/src/locales/cn/components/helpCenter.json b/src/web/src/locales/cn/components/helpCenter.json new file mode 100644 index 000000000..e3992dbb1 --- /dev/null +++ b/src/web/src/locales/cn/components/helpCenter.json @@ -0,0 +1,195 @@ +{ + "helpCenter.title": "帮助中心", + "helpCenter.button.tooltip": "查看使用说明", + "helpCenter.action.getStarted": "开始使用", + "helpCenter.entry.whatCanPathMarksDo": "路径标记能做什么?", + "helpCenter.topic.pathMark": "路径标记", + "helpCenter.pathMark.section.whatIs": "它是什么", + "helpCenter.pathMark.section.examples": "示例库", + "helpCenter.pathMark.section.comparison": "对比常规方案", + "helpCenter.pathMark.section.concepts": "概念速查", + "helpCenter.pathMark.whatIs.headline": "教 Bakabase 看懂你现有的文件夹结构", + "helpCenter.pathMark.whatIs.intro": "你不需要为 Bakabase 重新整理文件。路径标记是一组作用在文件路径上的规则:它读取你现有的目录结构,自动识别出资源、为资源填上属性值、并把资源归入媒体库。文件夹怎么放,完全由你决定。", + "helpCenter.pathMark.whatIs.diagram.treeTitle": "你的文件夹(示例)", + "helpCenter.pathMark.whatIs.diagram.resultTitle": "同步后 Bakabase 中的结果", + "helpCenter.pathMark.whatIs.diagram.resultResources": "2 个资源:星际穿越、肖申克的救赎", + "helpCenter.pathMark.whatIs.diagram.resultProperties": "属性「类型」自动填入:科幻、剧情", + "helpCenter.pathMark.whatIs.diagram.resultLibrary": "两者都归入「电影」媒体库", + "helpCenter.pathMark.whatIs.diagram.resultNote": "之后即可在资源页按「类型」筛选、按媒体库浏览。", + "helpCenter.pathMark.whatIs.markTypes.title": "三种标记类型", + "helpCenter.pathMark.whatIs.markTypes.resource.name": "资源标记", + "helpCenter.pathMark.whatIs.markTypes.resource.desc": "决定哪些文件或文件夹是「资源」——Bakabase 管理的基本单位。可按层级或正则匹配,任意深度。", + "helpCenter.pathMark.whatIs.markTypes.property.name": "属性标记", + "helpCenter.pathMark.whatIs.markTypes.property.desc": "为匹配到的资源自动填属性值(标签、分类、作者等)。值可以固定,也可以从路径中动态提取。", + "helpCenter.pathMark.whatIs.markTypes.mediaLibrary.name": "媒体库标记", + "helpCenter.pathMark.whatIs.markTypes.mediaLibrary.desc": "把匹配到的资源归入媒体库。媒体库也可以按目录名动态创建。", + "helpCenter.pathMark.whatIs.markTypes.resourceIsCore": "资源标记是一切的基础:只有被资源标记匹配到的路径才会成为资源,属性标记和媒体库标记只对已有资源生效。", + "helpCenter.pathMark.whatIs.workflow.title": "使用流程", + "helpCenter.pathMark.whatIs.workflow.step1.title": "选择目录", + "helpCenter.pathMark.whatIs.workflow.step1.desc": "在目录树中找到要管理的文件夹。", + "helpCenter.pathMark.whatIs.workflow.step2.title": "添加标记", + "helpCenter.pathMark.whatIs.workflow.step2.desc": "右键或点击「添加标记」,配置匹配规则,实时预览会命中的路径。", + "helpCenter.pathMark.whatIs.workflow.step3.title": "同步生成数据", + "helpCenter.pathMark.whatIs.workflow.step3.desc": "同步会按标记生成资源、属性值和媒体库关联。", + "helpCenter.pathMark.whatIs.workflow.syncNote": "默认开启自动同步,标记变更后会自动生效;也可以在设置中关闭,改用「待同步」手动触发。", + "helpCenter.pathMark.badge.resource": "资源", + "helpCenter.pathMark.badge.property": "属性", + "helpCenter.pathMark.badge.mediaLibrary": "媒体库", + "helpCenter.pathMark.badge.libraryMovies": "媒体库:电影", + "helpCenter.pathMark.badge.libraryAnime": "媒体库:动画", + "helpCenter.pathMark.badge.libraryAuto": "→ 同名媒体库", + "helpCenter.pathMark.badge.genreDynamic": "类型 ← 目录名", + "helpCenter.pathMark.badge.seasonDynamic": "季度 ← 目录名", + "helpCenter.pathMark.badge.artistDynamic": "艺术家 ← 目录名", + "helpCenter.pathMark.badge.statusOngoing": "状态 = 连载中", + "helpCenter.pathMark.badge.statusCompleted": "状态 = 已完结", + "helpCenter.pathMark.badge.boundary": "资源 · 边界", + "helpCenter.pathMark.badge.autoAdded": "自动收录", + "helpCenter.pathMark.badge.identityFile": "身份标记文件", + "helpCenter.pathMark.node.movies": "电影", + "helpCenter.pathMark.node.scifi": "科幻", + "helpCenter.pathMark.node.drama": "剧情", + "helpCenter.pathMark.node.interstellar": "星际穿越", + "helpCenter.pathMark.node.inception": "盗梦空间", + "helpCenter.pathMark.node.shawshank": "肖申克的救赎", + "helpCenter.pathMark.node.anime": "动画", + "helpCenter.pathMark.node.frieren": "葬送的芙莉莲", + "helpCenter.pathMark.node.ep01": "第01话.mkv", + "helpCenter.pathMark.node.ep02": "第02话.mkv", + "helpCenter.pathMark.node.manga": "漫画", + "helpCenter.pathMark.node.onePiece": "[尾田荣一郎] 海贼王", + "helpCenter.pathMark.node.slamDunk": "[井上雄彦] 灌篮高手", + "helpCenter.pathMark.node.dragonBall": "龙珠", + "helpCenter.pathMark.node.music": "音乐", + "helpCenter.pathMark.node.hisaishi": "久石让", + "helpCenter.pathMark.node.laputaOst": "天空之城 原声集", + "helpCenter.pathMark.node.collections": "资料库", + "helpCenter.pathMark.node.ongoing": "连载中", + "helpCenter.pathMark.node.completed": "已完结", + "helpCenter.pathMark.node.avatarBd": "阿凡达 BD原盘", + "helpCenter.pathMark.node.downloads": "下载", + "helpCenter.pathMark.node.newDownload": "昨晚新增的剧集", + "helpCenter.pathMark.ability.layer": "层级匹配", + "helpCenter.pathMark.ability.anyLevel": "任意层级", + "helpCenter.pathMark.ability.regex": "正则匹配", + "helpCenter.pathMark.ability.dynamicProperty": "动态属性", + "helpCenter.pathMark.ability.dynamicMediaLibrary": "动态媒体库", + "helpCenter.pathMark.ability.filter": "文件类型过滤", + "helpCenter.pathMark.ability.boundary": "资源边界", + "helpCenter.pathMark.ability.scope": "应用范围", + "helpCenter.pathMark.ability.priority": "优先级", + "helpCenter.pathMark.ability.multiMark": "多标记叠加", + "helpCenter.pathMark.ability.schedule": "定时同步", + "helpCenter.pathMark.ability.identity": "身份保持", + "helpCenter.pathMark.examples.intro": "每个示例都给出文件夹结构、需要添加的标记和同步后的结果。颜色对应标记类型:绿色 = 资源,蓝色 = 属性,紫色 = 媒体库。", + "helpCenter.pathMark.examples.movieBasic.title": "电影收藏入门:每个文件夹一部电影", + "helpCenter.pathMark.examples.movieBasic.desc": "最常见的结构:电影根目录下每个子文件夹存放一部电影。", + "helpCenter.pathMark.examples.movieBasic.mark1": "资源标记:层级 = 1,把根目录下的每个子文件夹识别为一个资源。", + "helpCenter.pathMark.examples.movieBasic.mark2": "媒体库标记:固定关联到「电影」媒体库,范围含子目录。", + "helpCenter.pathMark.examples.movieBasic.result": "「星际穿越」「盗梦空间」成为资源,并出现在「电影」媒体库中。", + "helpCenter.pathMark.examples.movieGenre.title": "按类型分层的电影库", + "helpCenter.pathMark.examples.movieGenre.desc": "中间多了一层类型目录?资源可以在任意层级,中间层还能变成属性。", + "helpCenter.pathMark.examples.movieGenre.mark1": "资源标记:层级 = 2,把第二层子文件夹识别为资源。", + "helpCenter.pathMark.examples.movieGenre.mark2": "属性标记:属性「类型」,值动态取自第 1 层目录名。", + "helpCenter.pathMark.examples.movieGenre.result": "资源自动带上「类型 = 科幻 / 剧情」,无需手动填写,即可按类型筛选。", + "helpCenter.pathMark.examples.animeSeason.title": "动画按季度追番", + "helpCenter.pathMark.examples.animeSeason.desc": "按季度归档的番剧文件夹,季度目录名直接变成属性值。", + "helpCenter.pathMark.examples.animeSeason.mark1": "资源标记:层级 = 2,每部番剧的文件夹是一个资源。", + "helpCenter.pathMark.examples.animeSeason.mark2": "属性标记:属性「季度」,值动态取自第 1 层目录名(如 2024-04)。", + "helpCenter.pathMark.examples.animeSeason.result": "每季新建一个文件夹即可,季度属性自动生成,可按季度回顾。", + "helpCenter.pathMark.examples.mangaAuthor.title": "漫画按作者归档(正则提取)", + "helpCenter.pathMark.examples.mangaAuthor.desc": "文件夹名是「[作者] 作品名」的形式?用正则把作者提取成属性。", + "helpCenter.pathMark.examples.mangaAuthor.mark1": "资源标记:层级 = 1,每个作品文件夹是一个资源。", + "helpCenter.pathMark.examples.mangaAuthor.mark2": "属性标记:属性「作者」,值用正则 \\[(.+?)\\] 从文件夹名中提取。", + "helpCenter.pathMark.examples.mangaAuthor.result": "作者属性自动填好,点开某位作者即可看到其全部作品。", + "helpCenter.pathMark.examples.musicLibrary.title": "音乐库:艺术家 / 专辑两层结构", + "helpCenter.pathMark.examples.musicLibrary.desc": "专辑文件夹是资源,上一层的艺术家目录名变成属性。", + "helpCenter.pathMark.examples.musicLibrary.mark1": "资源标记:层级 = 2,每个专辑文件夹是一个资源。", + "helpCenter.pathMark.examples.musicLibrary.mark2": "属性标记:属性「艺术家」,值动态取自第 1 层目录名。", + "helpCenter.pathMark.examples.musicLibrary.result": "按艺术家浏览专辑;同样的思路适用于任何「人名 / 作品」结构。", + "helpCenter.pathMark.examples.autoLibraries.title": "顶层文件夹自动变成媒体库", + "helpCenter.pathMark.examples.autoLibraries.desc": "不想手动建媒体库?让第一层目录名直接决定媒体库。", + "helpCenter.pathMark.examples.autoLibraries.mark1": "媒体库标记:动态模式,取第 1 层目录名作为媒体库名,不存在则自动创建。", + "helpCenter.pathMark.examples.autoLibraries.mark2": "资源标记:层级 = 2,识别每个媒体库下的内容。", + "helpCenter.pathMark.examples.autoLibraries.result": "新建一个顶层文件夹就等于新建一个媒体库,零额外配置。", + "helpCenter.pathMark.examples.multiMarks.title": "一套文件,多个标记叠加", + "helpCenter.pathMark.examples.multiMarks.desc": "同一个资源可以同时命中多个标记,各自生效互不影响。", + "helpCenter.pathMark.examples.multiMarks.mark1": "媒体库标记:根目录 → 「动画」媒体库。", + "helpCenter.pathMark.examples.multiMarks.mark2": "属性标记:「状态 = 连载中」固定值,范围含子目录。", + "helpCenter.pathMark.examples.multiMarks.mark3": "属性标记:「季度」动态取自目录名。", + "helpCenter.pathMark.examples.multiMarks.mark4": "资源标记:层级 = 3。", + "helpCenter.pathMark.examples.multiMarks.result": "「葬送的芙莉莲」同时获得媒体库、状态、季度三项数据;多个标记的处理顺序可用优先级控制。", + "helpCenter.pathMark.examples.extensionFilter.title": "只把视频文件当资源", + "helpCenter.pathMark.examples.extensionFilter.desc": "文件夹里混着字幕、封面、元数据文件?用文件类型过滤只挑出正片。", + "helpCenter.pathMark.examples.extensionFilter.mark1": "资源标记:层级 = 2 + 仅文件 + 限制视频扩展名(可用扩展名组)。", + "helpCenter.pathMark.examples.extensionFilter.result": "只有 movie.mkv 成为资源,srt / jpg / nfo 不会进入资源列表。", + "helpCenter.pathMark.examples.resourceBoundary.title": "BD 原盘:识别到此为止", + "helpCenter.pathMark.examples.resourceBoundary.desc": "原盘、成套文件夹的内部结构不应再被拆成更多资源。", + "helpCenter.pathMark.examples.resourceBoundary.mark1": "资源标记:层级 = 1 + 开启「资源边界」。", + "helpCenter.pathMark.examples.resourceBoundary.result": "整个原盘文件夹是一个资源,内部的 BDMV / STREAM 结构不会再被识别。", + "helpCenter.pathMark.examples.scopeTag.title": "整个文件夹打同一个标签", + "helpCenter.pathMark.examples.scopeTag.desc": "把「已完结」目录下的所有资源统一标记,无需逐个设置。", + "helpCenter.pathMark.examples.scopeTag.mark1": "属性标记:「状态 = 已完结」固定值,应用范围 = 匹配项及子目录。", + "helpCenter.pathMark.examples.scopeTag.result": "移入「已完结」文件夹的作品自动获得该状态,移出后重新同步即更新。", + "helpCenter.pathMark.examples.scheduledSync.title": "下载目录定期自动收录", + "helpCenter.pathMark.examples.scheduledSync.desc": "常有新内容进来的目录,让标记定时重新同步。", + "helpCenter.pathMark.examples.scheduledSync.mark1": "资源标记:层级 = 1 + 开启「定时同步」(如每小时)。", + "helpCenter.pathMark.examples.scheduledSync.result": "新下载的内容会在下一次定时同步时自动成为资源,无需手动操作。", + "helpCenter.pathMark.examples.keepIdentity.title": "整理文件后,资源数据不丢失", + "helpCenter.pathMark.examples.keepIdentity.desc": "重命名或移动了资源文件夹,评分、属性、播放记录还在。", + "helpCenter.pathMark.examples.keepIdentity.mark1": "无需新标记:在路径标记设置中开启「保持资源身份标识」。", + "helpCenter.pathMark.examples.keepIdentity.result": "系统在资源文件夹中写入 bakabase.json 身份标记;路径变化后重新同步,原有数据自动跟随到新路径。", + "helpCenter.pathMark.comparison.intro": "与常见的两类管理方式相比,路径标记把「文件怎么放」和「数据怎么管」解耦:规则描述结构,数据自动生成。", + "helpCenter.pathMark.comparison.column.capability": "能力", + "helpCenter.pathMark.comparison.column.scraper": "常规刮削型媒体管理器", + "helpCenter.pathMark.comparison.column.manual": "手动分类 / 打标签工具", + "helpCenter.pathMark.comparison.column.bakabase": "Bakabase 路径标记", + "helpCenter.pathMark.comparison.row.noRestructure": "不要求特定目录结构,无需移动或重命名文件", + "helpCenter.pathMark.comparison.row.anyLevel": "资源可位于任意层级(同一目录树不同深度)", + "helpCenter.pathMark.comparison.row.regexMatch": "按正则表达式匹配路径", + "helpCenter.pathMark.comparison.row.dynamicProperty": "属性值从路径自动提取(层级 / 正则捕获)", + "helpCenter.pathMark.comparison.row.dynamicLibrary": "媒体库按目录名自动创建", + "helpCenter.pathMark.comparison.row.stackedRules": "同一套文件叠加多套规则", + "helpCenter.pathMark.comparison.row.preview": "规则生效前可预览影响范围", + "helpCenter.pathMark.comparison.row.anyFileType": "不限媒体类型(视频、漫画、音乐、任意文件)", + "helpCenter.pathMark.comparison.row.keepDataOnMove": "文件移动 / 重命名后保留已有数据", + "helpCenter.pathMark.comparison.row.portableRules": "规则可复制、粘贴、批量迁移", + "helpCenter.pathMark.comparison.legend.yes": "支持", + "helpCenter.pathMark.comparison.legend.partial": "部分支持", + "helpCenter.pathMark.comparison.legend.no": "不支持", + "helpCenter.pathMark.comparison.note": "「常规刮削型媒体管理器」泛指要求按固定目录规范存放、以刮削元数据为主的工具;「手动分类 / 打标签工具」泛指逐个添加条目、手动维护标签的工具。具体产品能力各有差异,此表仅概括常见形态。", + "helpCenter.pathMark.concept.layer.name": "层级", + "helpCenter.pathMark.concept.layer.short": "以目录深度定位匹配项", + "helpCenter.pathMark.concept.layer.long": "层级从选中的路径开始数:0 = 选中的路径本身,1 = 它的下一层子项,以此类推。层级 = 1 表示「选中目录下的每个子文件夹 / 文件」。", + "helpCenter.pathMark.concept.layer.example": "例:选中「电影」目录,层级 = 1 会匹配「电影」下的每个子文件夹。", + "helpCenter.pathMark.concept.regex.name": "正则表达式", + "helpCenter.pathMark.concept.regex.short": "以模式匹配路径", + "helpCenter.pathMark.concept.regex.long": "对从选中路径开始的相对路径做正则匹配,适合目录名有固定模式(如 [作者] 作品名)的场景。正则的捕获组还可以用于动态提取属性值或媒体库名。", + "helpCenter.pathMark.concept.regex.example": "例:\\[(.+?)\\] 可以从「[尾田荣一郎] 海贼王」中提取出「尾田荣一郎」。", + "helpCenter.pathMark.concept.matchMode.name": "匹配模式", + "helpCenter.pathMark.concept.matchMode.short": "层级与正则二选一", + "helpCenter.pathMark.concept.matchMode.long": "决定标记如何找到匹配项:层级模式按目录深度匹配,简单直观;正则模式按路径模式匹配,更灵活。只想选中当前路径时,用层级 = 0。", + "helpCenter.pathMark.concept.applyScope.name": "应用范围", + "helpCenter.pathMark.concept.applyScope.short": "只作用于匹配项,还是连同其子内容", + "helpCenter.pathMark.concept.applyScope.long": "「仅匹配项」:资源路径必须与匹配项完全一致才生效。「匹配项及子目录」:匹配项本身及其内部任意子文件 / 子文件夹都生效。给整个文件夹统一打标签时用后者。", + "helpCenter.pathMark.concept.applyScope.example": "例:在「已完结」目录上配置「状态 = 已完结」并选择「匹配项及子目录」,目录下所有资源都会获得该状态。", + "helpCenter.pathMark.concept.dynamicValue.name": "动态值", + "helpCenter.pathMark.concept.dynamicValue.short": "属性值 / 媒体库名从路径中提取", + "helpCenter.pathMark.concept.dynamicValue.long": "属性标记和媒体库标记的值除了固定填写,还可以动态提取:按层级取某一层的目录名,或用正则捕获组从路径中截取。目录名变了,重新同步后值会跟着变。", + "helpCenter.pathMark.concept.dynamicValue.example": "例:属性「艺术家」动态取第 1 层目录名,「音乐/久石让/专辑」会得到「艺术家 = 久石让」。", + "helpCenter.pathMark.concept.priority.name": "优先级", + "helpCenter.pathMark.concept.priority.short": "多个标记的处理顺序", + "helpCenter.pathMark.concept.priority.long": "同一路径命中多个标记时,按优先级从小到大依次处理。数值越小越先处理,一般无需调整,只有多个标记互相影响时才需要。", + "helpCenter.pathMark.concept.sync.name": "同步", + "helpCenter.pathMark.concept.sync.short": "把标记变成实际数据", + "helpCenter.pathMark.concept.sync.long": "创建或修改标记后不会立刻生成数据。同步会读取标记并生成对应的资源、属性值和媒体库关联。默认开启自动同步(标记变更后自动运行);关闭后可用「待同步」按钮手动触发。", + "helpCenter.pathMark.concept.boundary.name": "资源边界", + "helpCenter.pathMark.concept.boundary.short": "阻止向下继续识别资源", + "helpCenter.pathMark.concept.boundary.long": "开启后,此标记匹配到的资源即为终点,其内部结构不会再被其他资源标记识别为更多资源。适合 BD 原盘、成套文件夹等应作为整体的内容。", + "helpCenter.pathMark.concept.scheduledSync.name": "定时同步", + "helpCenter.pathMark.concept.scheduledSync.short": "让标记按周期自动重新同步", + "helpCenter.pathMark.concept.scheduledSync.long": "为单个标记设置过期时间,到期后自动重新同步一次。适合内容持续变化的目录(如下载目录)。周期过短会频繁同步影响性能。", + "helpCenter.pathMark.concept.keepIdentity.name": "保持资源身份标识", + "helpCenter.pathMark.concept.keepIdentity.short": "移动文件不丢数据", + "helpCenter.pathMark.concept.keepIdentity.long": "开启后系统会在资源文件夹中生成 bakabase.json 标记文件。之后资源路径发生变化时,重新同步不会创建新资源,而是通过标记文件找回原资源并更新路径,评分、属性、播放记录全部保留。" +} diff --git a/src/web/src/locales/cn/pages/pathMarkConfig.json b/src/web/src/locales/cn/pages/pathMarkConfig.json index f295e0378..6ae80c67e 100644 --- a/src/web/src/locales/cn/pages/pathMarkConfig.json +++ b/src/web/src/locales/cn/pages/pathMarkConfig.json @@ -119,16 +119,13 @@ "pathMark.regex.description": "用于匹配从根路径开始的相对路径的正则表达式。", "pathMark.applyScope.matchedOnly.description": "仅应用于精确匹配的路径", "pathMark.applyScope.matchedAndSubdirectories.description": "应用于匹配的路径及其中的所有路径", - "pathMark.resource.matchMode.description": "可以基于您当前选择的路径进行扩展,如果您只希望选择当前路径,那么使用层级=0 即可。", "pathMark.resource.applyScope.matchedOnly.description": "仅当资源路径和您配置的匹配项完全重合时,才将其视为资源。", "pathMark.resource.applyScope.matchedAndSubdirectories.description": "当资源和您配置的匹配项完全重合,或是匹配项的任意子文件或文件夹时,才将其视作资源。", - "pathMark.property.matchMode.description": "可以基于您当前选择的路径进行扩展,如果您只希望选择当前路径,那么使用层级=0 即可。", "pathMark.property.applyScope.matchedOnly.description": "仅当资源路径和您配置的匹配项完全重合时,才为资源设置您配置的属性和属性值。", "pathMark.property.applyScope.matchedAndSubdirectories.description": "当资源和您配置的匹配项完全重合,或是匹配项的子文件或文件夹时,才为资源设置您配置的属性和属性值。", "pathMark.property.configDescription": "您可以选择,当资源匹配后,应该将它与哪个属性绑定,以及属性值应该设置成什么。", - "pathMark.mediaLibrary.matchMode.description": "可以基于您当前选择的路径进行扩展,如果您只希望选择当前路径,那么使用层级=0 即可。", "pathMark.mediaLibrary.applyScope.matchedOnly.description": "仅当资源路径和您配置的匹配项完全重合时,才将资源关联到您设置的媒体库。", "pathMark.mediaLibrary.applyScope.matchedAndSubdirectories.description": "当资源和您配置的匹配项完全重合,或是匹配项的子文件或文件夹时,才将资源关联到您设置的媒体库。", @@ -140,7 +137,6 @@ "pathMark.mediaLibrary.explanation": "媒体库标记:将资源归类到指定的媒体库中。", "pathMark.mediaLibrary.settingsDescription": "选择目标媒体库,或从路径自动提取。", "pathMark.mediaLibrary.importantNote": "媒体库标记不会创建资源,只有路径被「资源标记」匹配到的资源才会被关联到媒体库。", - "markDescription.layer.currentAndSubdirs": "当前层级 + 子目录", "markDescription.layer.current": "当前层级", "markDescription.layer.suffix": " 层", @@ -156,42 +152,10 @@ "markDescription.dirOnly": "仅目录", "markDescription.empty": "无描述", "markDescription.invalid": "配置无效", - "pathMarkConfig.tip.clickToEditRightClickDelete": "点击编辑,右键删除", "pathMarkConfig.status.syncProgress": "同步中: {{progress}}%", "pathMarkConfig.tip.recheckAfter": "重新检查于", "pathMarkConfig.action.viewAllMarks": "查看全部标记", - - "pathMarkGuide.welcome.title": "欢迎使用路径标记", - "pathMarkGuide.welcome.description": "路径标记帮助您通过在文件路径上定义规则来组织媒体库。您可以指定哪些文件是资源、分配属性,并将它们与媒体库关联。", - - "pathMarkGuide.markTypes.title": "三种标记类型", - "pathMarkGuide.markTypes.subtitle": "每种标记类型在组织内容方面都有不同的用途。", - "pathMarkGuide.markTypes.resource": "资源标记(核心)", - "pathMarkGuide.markTypes.resourceDesc": "将文件或文件夹标识为媒体库中的资源。使用层级或正则表达式匹配路径。", - "pathMarkGuide.markTypes.resourceImportant": "资源标记是整个系统的基础 - 只有设置了资源标记后,您才能在资源页面中查看和管理您的资源。没有资源标记,系统将无法知道要追踪哪些文件。", - "pathMarkGuide.markTypes.property": "属性标记", - "pathMarkGuide.markTypes.propertyDesc": "自动为匹配的资源分配属性值(如标签、分类)。", - "pathMarkGuide.markTypes.mediaLibrary": "媒体库标记", - "pathMarkGuide.markTypes.mediaLibraryDesc": "将资源与特定媒体库关联,便于更好地管理。", - - "pathMarkGuide.configure.title": "如何配置", - "pathMarkGuide.configure.subtitle": "按照以下步骤设置您的路径标记。", - "pathMarkGuide.configure.step1": "在文件树中导航到一个文件夹并选择它。", - "pathMarkGuide.configure.step2": "右键单击或使用「添加标记」按钮创建标记。", - "pathMarkGuide.configure.step3": "配置匹配规则并预览受影响的路径。", - - "pathMarkGuide.sync.title": "同步与管理", - "pathMarkGuide.sync.subtitle": "标记需要同步后才能生成实际数据。", - "pathMarkGuide.sync.explanation": "创建标记后并不会立刻生成资源。同步是读取您设置的标记并生成相应数据(资源、属性、媒体库关联)的过程。", - "pathMarkGuide.sync.point1": "默认情况下,自动同步已启用,会在标记变更后自动运行。您可以在设置中禁用自动同步。", - "pathMarkGuide.sync.point2": "如果禁用了自动同步,可以使用「待同步」按钮手动触发同步。", - - "pathMarkGuide.skip": "跳过", - "pathMarkGuide.previous": "上一步", - "pathMarkGuide.next": "下一步", - "pathMarkGuide.getStarted": "开始使用", - "pathMarkConfig.label.nextStepHint": "下一步:查看所有已配置的标记,或配置资源扩展来定义增强器、播放器等设置", "pathMarkConfig.action.goToResourceProfile": "配置资源扩展", "pathMarkConfig.label.keepResourcesOnPathChange": "保持资源身份标识", diff --git a/src/web/src/locales/en/components/helpCenter.json b/src/web/src/locales/en/components/helpCenter.json new file mode 100644 index 000000000..490b4a586 --- /dev/null +++ b/src/web/src/locales/en/components/helpCenter.json @@ -0,0 +1,195 @@ +{ + "helpCenter.title": "Help Center", + "helpCenter.button.tooltip": "View guide", + "helpCenter.action.getStarted": "Get Started", + "helpCenter.entry.whatCanPathMarksDo": "What can path marks do?", + "helpCenter.topic.pathMark": "Path Marks", + "helpCenter.pathMark.section.whatIs": "What Is It", + "helpCenter.pathMark.section.examples": "Examples", + "helpCenter.pathMark.section.comparison": "vs. Typical Tools", + "helpCenter.pathMark.section.concepts": "Concepts", + "helpCenter.pathMark.whatIs.headline": "Teach Bakabase to understand your existing folder structure", + "helpCenter.pathMark.whatIs.intro": "You don't need to reorganize your files for Bakabase. Path marks are rules applied to file paths: they read your existing directory structure, automatically identify resources, fill in property values, and assign resources to media libraries. How you arrange your folders is entirely up to you.", + "helpCenter.pathMark.whatIs.diagram.treeTitle": "Your folders (example)", + "helpCenter.pathMark.whatIs.diagram.resultTitle": "Result in Bakabase after sync", + "helpCenter.pathMark.whatIs.diagram.resultResources": "2 resources: Interstellar, The Shawshank Redemption", + "helpCenter.pathMark.whatIs.diagram.resultProperties": "Property \"Genre\" filled automatically: Sci-Fi, Drama", + "helpCenter.pathMark.whatIs.diagram.resultLibrary": "Both assigned to the \"Movies\" media library", + "helpCenter.pathMark.whatIs.diagram.resultNote": "You can then filter by \"Genre\" and browse by media library on the resource page.", + "helpCenter.pathMark.whatIs.markTypes.title": "Three Mark Types", + "helpCenter.pathMark.whatIs.markTypes.resource.name": "Resource Mark", + "helpCenter.pathMark.whatIs.markTypes.resource.desc": "Decides which files or folders are \"resources\" — the basic unit Bakabase manages. Match by layer or regex, at any depth.", + "helpCenter.pathMark.whatIs.markTypes.property.name": "Property Mark", + "helpCenter.pathMark.whatIs.markTypes.property.desc": "Automatically fills property values (tags, categories, authors, ...) for matched resources. Values can be fixed or dynamically extracted from the path.", + "helpCenter.pathMark.whatIs.markTypes.mediaLibrary.name": "Media Library Mark", + "helpCenter.pathMark.whatIs.markTypes.mediaLibrary.desc": "Assigns matched resources to a media library. Libraries can also be created dynamically from directory names.", + "helpCenter.pathMark.whatIs.markTypes.resourceIsCore": "Resource marks are the foundation of everything: only paths matched by a resource mark become resources. Property marks and media library marks only take effect on existing resources.", + "helpCenter.pathMark.whatIs.workflow.title": "How It Works", + "helpCenter.pathMark.whatIs.workflow.step1.title": "Pick a directory", + "helpCenter.pathMark.whatIs.workflow.step1.desc": "Find the folder you want to manage in the directory tree.", + "helpCenter.pathMark.whatIs.workflow.step2.title": "Add marks", + "helpCenter.pathMark.whatIs.workflow.step2.desc": "Right-click or use \"Add Mark\", configure the matching rule, and preview affected paths live.", + "helpCenter.pathMark.whatIs.workflow.step3.title": "Sync to generate data", + "helpCenter.pathMark.whatIs.workflow.step3.desc": "Sync reads your marks and generates resources, property values, and media library associations.", + "helpCenter.pathMark.whatIs.workflow.syncNote": "Auto-sync is on by default and runs after mark changes; you can disable it in settings and trigger sync manually via \"Pending Sync\".", + "helpCenter.pathMark.badge.resource": "Resource", + "helpCenter.pathMark.badge.property": "Property", + "helpCenter.pathMark.badge.mediaLibrary": "Library", + "helpCenter.pathMark.badge.libraryMovies": "Library: Movies", + "helpCenter.pathMark.badge.libraryAnime": "Library: Anime", + "helpCenter.pathMark.badge.libraryAuto": "→ library of same name", + "helpCenter.pathMark.badge.genreDynamic": "Genre ← folder name", + "helpCenter.pathMark.badge.seasonDynamic": "Season ← folder name", + "helpCenter.pathMark.badge.artistDynamic": "Artist ← folder name", + "helpCenter.pathMark.badge.statusOngoing": "Status = Ongoing", + "helpCenter.pathMark.badge.statusCompleted": "Status = Completed", + "helpCenter.pathMark.badge.boundary": "Resource · Boundary", + "helpCenter.pathMark.badge.autoAdded": "Auto-added", + "helpCenter.pathMark.badge.identityFile": "Identity file", + "helpCenter.pathMark.node.movies": "Movies", + "helpCenter.pathMark.node.scifi": "Sci-Fi", + "helpCenter.pathMark.node.drama": "Drama", + "helpCenter.pathMark.node.interstellar": "Interstellar", + "helpCenter.pathMark.node.inception": "Inception", + "helpCenter.pathMark.node.shawshank": "The Shawshank Redemption", + "helpCenter.pathMark.node.anime": "Anime", + "helpCenter.pathMark.node.frieren": "Frieren", + "helpCenter.pathMark.node.ep01": "E01.mkv", + "helpCenter.pathMark.node.ep02": "E02.mkv", + "helpCenter.pathMark.node.manga": "Manga", + "helpCenter.pathMark.node.onePiece": "[Eiichiro Oda] One Piece", + "helpCenter.pathMark.node.slamDunk": "[Takehiko Inoue] Slam Dunk", + "helpCenter.pathMark.node.dragonBall": "Dragon Ball", + "helpCenter.pathMark.node.music": "Music", + "helpCenter.pathMark.node.hisaishi": "Joe Hisaishi", + "helpCenter.pathMark.node.laputaOst": "Laputa OST", + "helpCenter.pathMark.node.collections": "Collections", + "helpCenter.pathMark.node.ongoing": "Ongoing", + "helpCenter.pathMark.node.completed": "Completed", + "helpCenter.pathMark.node.avatarBd": "Avatar BD", + "helpCenter.pathMark.node.downloads": "Downloads", + "helpCenter.pathMark.node.newDownload": "New episode from last night", + "helpCenter.pathMark.ability.layer": "Layer match", + "helpCenter.pathMark.ability.anyLevel": "Any depth", + "helpCenter.pathMark.ability.regex": "Regex match", + "helpCenter.pathMark.ability.dynamicProperty": "Dynamic property", + "helpCenter.pathMark.ability.dynamicMediaLibrary": "Dynamic library", + "helpCenter.pathMark.ability.filter": "File type filter", + "helpCenter.pathMark.ability.boundary": "Resource boundary", + "helpCenter.pathMark.ability.scope": "Apply scope", + "helpCenter.pathMark.ability.priority": "Priority", + "helpCenter.pathMark.ability.multiMark": "Stacked marks", + "helpCenter.pathMark.ability.schedule": "Scheduled sync", + "helpCenter.pathMark.ability.identity": "Identity keeping", + "helpCenter.pathMark.examples.intro": "Each example shows the folder structure, the marks to add, and the result after sync. Colors match mark types: green = resource, blue = property, purple = media library.", + "helpCenter.pathMark.examples.movieBasic.title": "Movie collection basics: one folder per movie", + "helpCenter.pathMark.examples.movieBasic.desc": "The most common structure: each subfolder under the movie root holds one movie.", + "helpCenter.pathMark.examples.movieBasic.mark1": "Resource mark: layer = 1 — each subfolder under the root becomes a resource.", + "helpCenter.pathMark.examples.movieBasic.mark2": "Media library mark: fixed to the \"Movies\" library, scope includes subdirectories.", + "helpCenter.pathMark.examples.movieBasic.result": "\"Interstellar\" and \"Inception\" become resources and show up in the \"Movies\" library.", + "helpCenter.pathMark.examples.movieGenre.title": "Movie library with a genre level", + "helpCenter.pathMark.examples.movieGenre.desc": "An extra genre level in between? Resources can live at any depth, and the middle level can become a property.", + "helpCenter.pathMark.examples.movieGenre.mark1": "Resource mark: layer = 2 — second-level subfolders become resources.", + "helpCenter.pathMark.examples.movieGenre.mark2": "Property mark: property \"Genre\", value dynamically taken from the layer-1 folder name.", + "helpCenter.pathMark.examples.movieGenre.result": "Resources automatically get \"Genre = Sci-Fi / Drama\" without manual input, ready for filtering.", + "helpCenter.pathMark.examples.animeSeason.title": "Anime by season", + "helpCenter.pathMark.examples.animeSeason.desc": "Anime archived by season — the season folder name becomes a property value directly.", + "helpCenter.pathMark.examples.animeSeason.mark1": "Resource mark: layer = 2 — each show's folder is one resource.", + "helpCenter.pathMark.examples.animeSeason.mark2": "Property mark: property \"Season\", value dynamically taken from the layer-1 folder name (e.g. 2024-04).", + "helpCenter.pathMark.examples.animeSeason.result": "Just create one folder per season — the season property is generated automatically.", + "helpCenter.pathMark.examples.mangaAuthor.title": "Manga by author (regex extraction)", + "helpCenter.pathMark.examples.mangaAuthor.desc": "Folder names like \"[Author] Title\"? Use a regex to extract the author into a property.", + "helpCenter.pathMark.examples.mangaAuthor.mark1": "Resource mark: layer = 1 — each title folder is one resource.", + "helpCenter.pathMark.examples.mangaAuthor.mark2": "Property mark: property \"Author\", value extracted from the folder name with regex \\[(.+?)\\].", + "helpCenter.pathMark.examples.mangaAuthor.result": "Author properties are filled automatically — open an author to see all their works.", + "helpCenter.pathMark.examples.musicLibrary.title": "Music library: artist / album structure", + "helpCenter.pathMark.examples.musicLibrary.desc": "Album folders are resources; the artist folder above them becomes a property.", + "helpCenter.pathMark.examples.musicLibrary.mark1": "Resource mark: layer = 2 — each album folder is one resource.", + "helpCenter.pathMark.examples.musicLibrary.mark2": "Property mark: property \"Artist\", value dynamically taken from the layer-1 folder name.", + "helpCenter.pathMark.examples.musicLibrary.result": "Browse albums by artist; the same idea applies to any \"person / work\" structure.", + "helpCenter.pathMark.examples.autoLibraries.title": "Top-level folders become media libraries", + "helpCenter.pathMark.examples.autoLibraries.desc": "Don't want to create libraries manually? Let layer-1 folder names decide the library.", + "helpCenter.pathMark.examples.autoLibraries.mark1": "Media library mark: dynamic mode — the layer-1 folder name becomes the library name, auto-created if missing.", + "helpCenter.pathMark.examples.autoLibraries.mark2": "Resource mark: layer = 2 — recognizes the content under each library.", + "helpCenter.pathMark.examples.autoLibraries.result": "Creating a new top-level folder equals creating a new media library — zero extra configuration.", + "helpCenter.pathMark.examples.multiMarks.title": "One file tree, multiple stacked marks", + "helpCenter.pathMark.examples.multiMarks.desc": "The same resource can match several marks at once — each takes effect independently.", + "helpCenter.pathMark.examples.multiMarks.mark1": "Media library mark: root → \"Anime\" library.", + "helpCenter.pathMark.examples.multiMarks.mark2": "Property mark: fixed \"Status = Ongoing\", scope includes subdirectories.", + "helpCenter.pathMark.examples.multiMarks.mark3": "Property mark: \"Season\" dynamically taken from the folder name.", + "helpCenter.pathMark.examples.multiMarks.mark4": "Resource mark: layer = 3.", + "helpCenter.pathMark.examples.multiMarks.result": "\"Frieren\" gets its library, status, and season all at once; use priority to control processing order when marks interact.", + "helpCenter.pathMark.examples.extensionFilter.title": "Only video files as resources", + "helpCenter.pathMark.examples.extensionFilter.desc": "Subtitles, posters, and metadata files mixed in? Use file type filtering to pick out the main video.", + "helpCenter.pathMark.examples.extensionFilter.mark1": "Resource mark: layer = 2 + files only + restrict to video extensions (extension groups supported).", + "helpCenter.pathMark.examples.extensionFilter.result": "Only movie.mkv becomes a resource — srt / jpg / nfo files stay out of the resource list.", + "helpCenter.pathMark.examples.resourceBoundary.title": "BD rips: recognition stops here", + "helpCenter.pathMark.examples.resourceBoundary.desc": "The internals of disc rips and other self-contained folders shouldn't be split into more resources.", + "helpCenter.pathMark.examples.resourceBoundary.mark1": "Resource mark: layer = 1 + \"resource boundary\" enabled.", + "helpCenter.pathMark.examples.resourceBoundary.result": "The whole disc folder is one resource; its internal BDMV / STREAM structure is never scanned further.", + "helpCenter.pathMark.examples.scopeTag.title": "Tag an entire folder at once", + "helpCenter.pathMark.examples.scopeTag.desc": "Give every resource under the \"Completed\" folder the same status without touching them one by one.", + "helpCenter.pathMark.examples.scopeTag.mark1": "Property mark: fixed \"Status = Completed\", apply scope = matched item and subdirectories.", + "helpCenter.pathMark.examples.scopeTag.result": "Works moved into the \"Completed\" folder get the status automatically; move them out and re-sync to update.", + "helpCenter.pathMark.examples.scheduledSync.title": "Auto-ingest a downloads folder", + "helpCenter.pathMark.examples.scheduledSync.desc": "For folders that keep receiving new content, let the mark re-sync on a schedule.", + "helpCenter.pathMark.examples.scheduledSync.mark1": "Resource mark: layer = 1 + \"scheduled sync\" enabled (e.g. hourly).", + "helpCenter.pathMark.examples.scheduledSync.result": "Newly downloaded content becomes resources at the next scheduled sync — no manual steps.", + "helpCenter.pathMark.examples.keepIdentity.title": "Reorganize files without losing data", + "helpCenter.pathMark.examples.keepIdentity.desc": "Renamed or moved a resource folder? Ratings, properties, and play history survive.", + "helpCenter.pathMark.examples.keepIdentity.mark1": "No new mark needed: enable \"Keep resource identity\" in the path mark settings.", + "helpCenter.pathMark.examples.keepIdentity.result": "Bakabase writes a bakabase.json identity file into each resource folder; after the path changes, re-sync finds the original resource and updates its path.", + "helpCenter.pathMark.comparison.intro": "Compared with the two common approaches, path marks decouple \"how files are arranged\" from \"how data is managed\": rules describe the structure, and data is generated automatically.", + "helpCenter.pathMark.comparison.column.capability": "Capability", + "helpCenter.pathMark.comparison.column.scraper": "Scraper-style media manager", + "helpCenter.pathMark.comparison.column.manual": "Manual tagging tool", + "helpCenter.pathMark.comparison.column.bakabase": "Bakabase Path Marks", + "helpCenter.pathMark.comparison.row.noRestructure": "No required directory layout — no moving or renaming files", + "helpCenter.pathMark.comparison.row.anyLevel": "Resources at any depth (even mixed depths in one tree)", + "helpCenter.pathMark.comparison.row.regexMatch": "Match paths with regular expressions", + "helpCenter.pathMark.comparison.row.dynamicProperty": "Property values extracted from paths (layer / regex capture)", + "helpCenter.pathMark.comparison.row.dynamicLibrary": "Media libraries auto-created from folder names", + "helpCenter.pathMark.comparison.row.stackedRules": "Multiple rule sets stacked on the same files", + "helpCenter.pathMark.comparison.row.preview": "Preview affected paths before rules take effect", + "helpCenter.pathMark.comparison.row.anyFileType": "Any media type (video, manga, music, arbitrary files)", + "helpCenter.pathMark.comparison.row.keepDataOnMove": "Data survives file moves / renames", + "helpCenter.pathMark.comparison.row.portableRules": "Rules can be copied, pasted, and migrated in bulk", + "helpCenter.pathMark.comparison.legend.yes": "Supported", + "helpCenter.pathMark.comparison.legend.partial": "Partial", + "helpCenter.pathMark.comparison.legend.no": "Not supported", + "helpCenter.pathMark.comparison.note": "\"Scraper-style media manager\" refers broadly to tools that require a fixed directory convention and focus on metadata scraping; \"manual tagging tool\" refers to tools where entries and tags are maintained by hand. Individual products vary — this table summarizes typical behavior.", + "helpCenter.pathMark.concept.layer.name": "Layer", + "helpCenter.pathMark.concept.layer.short": "Locate matches by directory depth", + "helpCenter.pathMark.concept.layer.long": "Layers are counted from the selected path: 0 = the selected path itself, 1 = its direct children, and so on. Layer = 1 means \"every subfolder / file directly under the selected directory\".", + "helpCenter.pathMark.concept.layer.example": "Example: with the \"Movies\" directory selected, layer = 1 matches every subfolder under \"Movies\".", + "helpCenter.pathMark.concept.regex.name": "Regular Expression", + "helpCenter.pathMark.concept.regex.short": "Match paths by pattern", + "helpCenter.pathMark.concept.regex.long": "Matches the relative path from the selected directory against a regex — ideal when folder names follow a pattern (like \"[Author] Title\"). Capture groups can also feed dynamic property values or library names.", + "helpCenter.pathMark.concept.regex.example": "Example: \\[(.+?)\\] extracts \"Eiichiro Oda\" from \"[Eiichiro Oda] One Piece\".", + "helpCenter.pathMark.concept.matchMode.name": "Match Mode", + "helpCenter.pathMark.concept.matchMode.short": "Layer or regex — pick one", + "helpCenter.pathMark.concept.matchMode.long": "Decides how a mark finds its matches: layer mode matches by directory depth and is simple and intuitive; regex mode matches by path pattern and is more flexible. To select just the current path, use layer = 0.", + "helpCenter.pathMark.concept.applyScope.name": "Apply Scope", + "helpCenter.pathMark.concept.applyScope.short": "Matched item only, or its children too", + "helpCenter.pathMark.concept.applyScope.long": "\"Matched only\": the resource path must equal the matched item exactly. \"Matched and subdirectories\": the matched item and anything inside it are affected. Use the latter to tag an entire folder at once.", + "helpCenter.pathMark.concept.applyScope.example": "Example: configure \"Status = Completed\" on the \"Completed\" directory with \"matched and subdirectories\" — every resource inside gets the status.", + "helpCenter.pathMark.concept.dynamicValue.name": "Dynamic Value", + "helpCenter.pathMark.concept.dynamicValue.short": "Property values / library names extracted from paths", + "helpCenter.pathMark.concept.dynamicValue.long": "Besides fixed values, property and media library marks can extract values dynamically: take a folder name at a given layer, or capture part of the path with a regex group. When folder names change, values follow after the next sync.", + "helpCenter.pathMark.concept.dynamicValue.example": "Example: property \"Artist\" dynamically taking the layer-1 folder name turns \"Music/Joe Hisaishi/Album\" into \"Artist = Joe Hisaishi\".", + "helpCenter.pathMark.concept.priority.name": "Priority", + "helpCenter.pathMark.concept.priority.short": "Processing order of multiple marks", + "helpCenter.pathMark.concept.priority.long": "When one path matches several marks, they are processed in ascending priority. Smaller runs first. Usually no adjustment is needed — only when marks affect each other.", + "helpCenter.pathMark.concept.sync.name": "Sync", + "helpCenter.pathMark.concept.sync.short": "Turn marks into actual data", + "helpCenter.pathMark.concept.sync.long": "Creating or editing a mark doesn't generate data immediately. Sync reads your marks and generates the corresponding resources, property values, and library associations. Auto-sync is on by default (runs after mark changes); when disabled, use the \"Pending Sync\" button.", + "helpCenter.pathMark.concept.boundary.name": "Resource Boundary", + "helpCenter.pathMark.concept.boundary.short": "Stop recognizing resources further down", + "helpCenter.pathMark.concept.boundary.long": "When enabled, a resource matched by this mark is terminal — its internal structure is never recognized as more resources by other resource marks. Ideal for disc rips and other folders that should stay whole.", + "helpCenter.pathMark.concept.scheduledSync.name": "Scheduled Sync", + "helpCenter.pathMark.concept.scheduledSync.short": "Re-sync a mark periodically", + "helpCenter.pathMark.concept.scheduledSync.long": "Give a single mark an expiration; it re-syncs automatically when due. Great for folders whose content keeps changing (e.g. downloads). Too short an interval causes frequent syncs and hurts performance.", + "helpCenter.pathMark.concept.keepIdentity.name": "Keep Resource Identity", + "helpCenter.pathMark.concept.keepIdentity.short": "Move files without losing data", + "helpCenter.pathMark.concept.keepIdentity.long": "When enabled, Bakabase writes a bakabase.json identity file into each resource folder. If the resource path later changes, re-sync finds the original resource through the file and updates its path — ratings, properties, and play history are all preserved." +} diff --git a/src/web/src/locales/en/pages/pathMarkConfig.json b/src/web/src/locales/en/pages/pathMarkConfig.json index 76c5f833c..2d7fb2861 100644 --- a/src/web/src/locales/en/pages/pathMarkConfig.json +++ b/src/web/src/locales/en/pages/pathMarkConfig.json @@ -119,16 +119,13 @@ "pathMark.regex.description": "Regular expression pattern to match against the relative path from root.", "pathMark.applyScope.matchedOnly.description": "Only apply to the exact matched path", "pathMark.applyScope.matchedAndSubdirectories.description": "Apply to matched path and all paths inside it", - "pathMark.resource.matchMode.description": "You can expand from the currently selected path. If you only want to select the current path, use layer=0.", "pathMark.resource.applyScope.matchedOnly.description": "Only treat as a resource when the resource path exactly matches the configured pattern.", "pathMark.resource.applyScope.matchedAndSubdirectories.description": "Treat as a resource when the resource path exactly matches the configured pattern, or is any file or folder within the match.", - "pathMark.property.matchMode.description": "You can expand from the currently selected path. If you only want to select the current path, use layer=0.", "pathMark.property.applyScope.matchedOnly.description": "Only set the configured property and value when the resource path exactly matches the configured pattern.", "pathMark.property.applyScope.matchedAndSubdirectories.description": "Set the configured property and value when the resource path exactly matches the configured pattern, or is a file or folder within the match.", "pathMark.property.configDescription": "Choose which property to bind to a matched resource, and what value to set.", - "pathMark.mediaLibrary.matchMode.description": "You can expand from the currently selected path. If you only want to select the current path, use layer=0.", "pathMark.mediaLibrary.applyScope.matchedOnly.description": "Only associate the resource with the configured media library when the resource path exactly matches the configured pattern.", "pathMark.mediaLibrary.applyScope.matchedAndSubdirectories.description": "Associate the resource with the configured media library when the resource path exactly matches the configured pattern, or is a file or folder within the match.", @@ -140,7 +137,6 @@ "pathMark.mediaLibrary.explanation": "Media Library Mark: Categorize resources into a specific media library.", "pathMark.mediaLibrary.settingsDescription": "Select a target library, or extract it from the path automatically.", "pathMark.mediaLibrary.importantNote": "Media library marks don't create resources. Only resources whose paths match a Resource Mark will be linked to the media library.", - "markDescription.layer.currentAndSubdirs": "This layer + subdirs", "markDescription.layer.current": "This layer", "markDescription.layer.suffix": " layer", @@ -156,42 +152,10 @@ "markDescription.dirOnly": "Dirs only", "markDescription.empty": "No description", "markDescription.invalid": "Invalid config", - "pathMarkConfig.tip.clickToEditRightClickDelete": "Click to edit, right-click to delete", "pathMarkConfig.status.syncProgress": "Syncing: {{progress}}%", "pathMarkConfig.tip.recheckAfter": "Re-check after", "pathMarkConfig.action.viewAllMarks": "View All Marks", - - "pathMarkGuide.welcome.title": "Welcome to Path Marks", - "pathMarkGuide.welcome.description": "Path Marks help you organize your media library by defining rules on file paths. You can specify which files are resources, assign properties, and associate them with media libraries.", - - "pathMarkGuide.markTypes.title": "Three Types of Marks", - "pathMarkGuide.markTypes.subtitle": "Each mark type serves a different purpose in organizing your content.", - "pathMarkGuide.markTypes.resource": "Resource Mark (Core)", - "pathMarkGuide.markTypes.resourceDesc": "Identifies files or folders as resources in your library. Use layer or regex to match paths.", - "pathMarkGuide.markTypes.resourceImportant": "Resource Mark is the foundation - only after setting it up can you view and manage your resources on the Resource page. Without it, the system won't know which files to track.", - "pathMarkGuide.markTypes.property": "Property Mark", - "pathMarkGuide.markTypes.propertyDesc": "Assigns property values (like tags, categories) to matched resources automatically.", - "pathMarkGuide.markTypes.mediaLibrary": "Media Library Mark", - "pathMarkGuide.markTypes.mediaLibraryDesc": "Associates resources with a specific media library for better organization.", - - "pathMarkGuide.configure.title": "How to Configure", - "pathMarkGuide.configure.subtitle": "Follow these steps to set up your path marks.", - "pathMarkGuide.configure.step1": "Navigate to a folder in the file tree and select it.", - "pathMarkGuide.configure.step2": "Right-click or use the Add Mark button to create marks.", - "pathMarkGuide.configure.step3": "Configure the matching rules and preview the affected paths.", - - "pathMarkGuide.sync.title": "Sync & Management", - "pathMarkGuide.sync.subtitle": "Marks need to be synced to generate actual data.", - "pathMarkGuide.sync.explanation": "Creating marks alone doesn't immediately create resources. Syncing is the process that reads your marks and generates the corresponding data (resources, properties, library associations).", - "pathMarkGuide.sync.point1": "By default, auto-sync is enabled and will run automatically after marks change. You can disable auto-sync in settings if needed.", - "pathMarkGuide.sync.point2": "If auto-sync is disabled, use the \"Pending Sync\" button to manually trigger synchronization.", - - "pathMarkGuide.skip": "Skip", - "pathMarkGuide.previous": "Previous", - "pathMarkGuide.next": "Next", - "pathMarkGuide.getStarted": "Get Started", - "pathMarkConfig.label.nextStepHint": "Next step: View all configured marks, or configure Resource Profiles to define enhancers, players, and other settings", "pathMarkConfig.action.goToResourceProfile": "Configure Resource Profiles", "pathMarkConfig.label.keepResourcesOnPathChange": "Keep resource identity on path change", diff --git a/src/web/src/pages/media-library/index.tsx b/src/web/src/pages/media-library/index.tsx index e957f20a2..fb62e070f 100644 --- a/src/web/src/pages/media-library/index.tsx +++ b/src/web/src/pages/media-library/index.tsx @@ -35,6 +35,7 @@ import { import { buildColorValueString } from "@/components/bakaui/components/ColorPicker"; import { EditableValue } from "@/components/EditableValue"; import { MediaLibraryTerm } from "@/components/Chips/Terms"; +import { HelpCenterButton } from "@/components/HelpCenter"; import { serializeStandardValue } from "@/components/StandardValue"; const MediaLibraryPage = () => { @@ -245,6 +246,7 @@ const MediaLibraryPage = () => { > {t("mediaLibrary.action.goToPathMarkConfig")} + @@ -436,6 +438,7 @@ const MediaLibraryPage = () => { > {t("mediaLibrary.action.goToPathMarkConfig")} + ); diff --git a/src/web/src/pages/path-mark-config/components/MarkConfigModal/index.tsx b/src/web/src/pages/path-mark-config/components/MarkConfigModal/index.tsx index 6f1408c0d..c70048466 100644 --- a/src/web/src/pages/path-mark-config/components/MarkConfigModal/index.tsx +++ b/src/web/src/pages/path-mark-config/components/MarkConfigModal/index.tsx @@ -16,6 +16,7 @@ import { usePreview } from "./hooks/usePreview"; import { PathMarkType, PathMarkApplyScope } from "@/sdk/constants"; import { Modal, Switch, DurationInput, Button, toast } from "@/components/bakaui"; import { ResourceTerm, PropertyTerm, MediaLibraryTerm } from "@/components/Chips/Terms"; +import { HelpCenterButton } from "@/components/HelpCenter"; import BApi from "@/sdk/BApi"; const DEFAULT_EXPIRES_IN_SECONDS = 3600; // 1 hour @@ -176,6 +177,7 @@ const MarkConfigModal = ({ ({t("pathMarkConfig.label.pathCount", { count: pathCount })}) )} + } onDestroyed={onDestroyed} diff --git a/src/web/src/pages/path-mark-config/components/PathMarkGuide/PathMarkGuideModal.tsx b/src/web/src/pages/path-mark-config/components/PathMarkGuide/PathMarkGuideModal.tsx deleted file mode 100644 index 9a6813d91..000000000 --- a/src/web/src/pages/path-mark-config/components/PathMarkGuide/PathMarkGuideModal.tsx +++ /dev/null @@ -1,265 +0,0 @@ -"use client"; - -import type { CarouselRef } from "antd/es/carousel"; - -import { useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { - AiOutlineFolder, - AiOutlineFileAdd, - AiOutlineTags, - AiOutlineAppstore, - AiOutlineSync, -} from "react-icons/ai"; -import { MdVideoLibrary } from "react-icons/md"; - -import Modal from "@/components/bakaui/components/Modal"; -import Carousel from "@/components/bakaui/components/Carousel"; -import { Button } from "@/components/bakaui"; - -interface PathMarkGuideModalProps { - visible: boolean; - onComplete: () => void; -} - -const TOTAL_SLIDES = 4; - -const PathMarkGuideModal = ({ visible, onComplete }: PathMarkGuideModalProps) => { - const { t } = useTranslation(); - const carouselRef = useRef(null); - const [currentSlide, setCurrentSlide] = useState(0); - - const isFirst = currentSlide === 0; - const isLast = currentSlide === TOTAL_SLIDES - 1; - - const handlePrev = () => { - carouselRef.current?.prev(); - }; - - const handleNext = () => { - if (isLast) { - onComplete(); - } else { - carouselRef.current?.next(); - } - }; - - const handleSkip = () => { - onComplete(); - }; - - const handleSlideChange = (current: number) => { - setCurrentSlide(current); - }; - - return ( - -
- - {/* Slide 1: Welcome / Introduction */} -
-
-
- -
- -

- {t("pathMarkGuide.welcome.title")} -

- -

- {t("pathMarkGuide.welcome.description")} -

-
-
- - {/* Slide 2: Three Mark Types */} -
-
-
- -
- -

- {t("pathMarkGuide.markTypes.title")} -

- -

- {t("pathMarkGuide.markTypes.subtitle")} -

- -
    - {/* Resource Mark - highlighted as core */} -
  • -
    - -
    -
    - - {t("pathMarkGuide.markTypes.resource")} - -

    - {t("pathMarkGuide.markTypes.resourceDesc")} -

    -
    -
  • -
  • -
    - -
    -
    - - {t("pathMarkGuide.markTypes.property")} - -

    - {t("pathMarkGuide.markTypes.propertyDesc")} -

    -
    -
  • -
  • -
    - -
    -
    - - {t("pathMarkGuide.markTypes.mediaLibrary")} - -

    - {t("pathMarkGuide.markTypes.mediaLibraryDesc")} -

    -
    -
  • -
- - {/* Important note about Resource Mark */} -
-

- {t("pathMarkGuide.markTypes.resourceImportant")} -

-
-
-
- - {/* Slide 3: How to Configure */} -
-
-
- -
- -

- {t("pathMarkGuide.configure.title")} -

- -

- {t("pathMarkGuide.configure.subtitle")} -

- -
    -
  • -
    - 1 -
    - - {t("pathMarkGuide.configure.step1")} - -
  • -
  • -
    - 2 -
    - - {t("pathMarkGuide.configure.step2")} - -
  • -
  • -
    - 3 -
    - - {t("pathMarkGuide.configure.step3")} - -
  • -
-
-
- - {/* Slide 4: Sync & Management */} -
-
-
- -
- -

- {t("pathMarkGuide.sync.title")} -

- -

- {t("pathMarkGuide.sync.subtitle")} -

- -
-

{t("pathMarkGuide.sync.explanation")}

-
- -
    -
  • -
    - 1 -
    - {t("pathMarkGuide.sync.point1")} -
  • -
  • -
    - 2 -
    - {t("pathMarkGuide.sync.point2")} -
  • -
-
-
-
- - {/* Progress dots */} -
- {Array.from({ length: TOTAL_SLIDES }).map((_, index) => ( -
- ))} -
- - {/* Navigation buttons */} -
- -
- - -
-
-
- - ); -}; - -export default PathMarkGuideModal; diff --git a/src/web/src/pages/path-mark-config/components/PathMarkGuide/index.ts b/src/web/src/pages/path-mark-config/components/PathMarkGuide/index.ts deleted file mode 100644 index 16841ed64..000000000 --- a/src/web/src/pages/path-mark-config/components/PathMarkGuide/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as PathMarkGuideModal } from "./PathMarkGuideModal"; -export { default as usePathMarkGuide } from "./usePathMarkGuide"; diff --git a/src/web/src/pages/path-mark-config/components/PathMarkGuide/usePathMarkGuide.ts b/src/web/src/pages/path-mark-config/components/PathMarkGuide/usePathMarkGuide.ts deleted file mode 100644 index d5fe4b3f8..000000000 --- a/src/web/src/pages/path-mark-config/components/PathMarkGuide/usePathMarkGuide.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; - -const GUIDE_KEY = "bakabase-path-mark-guide-completed"; - -export const usePathMarkGuide = () => { - const [showGuide, setShowGuide] = useState(false); - - useEffect(() => { - if (typeof window === "undefined" || typeof localStorage === "undefined") { - return; - } - - const completed = localStorage.getItem(GUIDE_KEY); - - if (!completed) { - setShowGuide(true); - } - }, []); - - const completeGuide = useCallback(() => { - if (typeof localStorage !== "undefined") { - localStorage.setItem(GUIDE_KEY, "true"); - } - setShowGuide(false); - }, []); - - const resetGuide = useCallback(() => { - if (typeof localStorage !== "undefined") { - localStorage.removeItem(GUIDE_KEY); - } - setShowGuide(true); - }, []); - - return { - showGuide, - completeGuide, - resetGuide, - }; -}; - -export default usePathMarkGuide; diff --git a/src/web/src/pages/path-mark-config/components/PathMarkSettingsButton.tsx b/src/web/src/pages/path-mark-config/components/PathMarkSettingsButton.tsx index 793e9bc81..673610942 100644 --- a/src/web/src/pages/path-mark-config/components/PathMarkSettingsButton.tsx +++ b/src/web/src/pages/path-mark-config/components/PathMarkSettingsButton.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { AiOutlineSetting } from "react-icons/ai"; import { Button, Modal, Checkbox, toast } from "@/components/bakaui"; +import { HelpCenterButton } from "@/components/HelpCenter"; import { useBakabaseContext } from "@/components/ContextProvider/BakabaseContextProvider"; import { useResourceOptionsStore } from "@/stores/options"; import BApi from "@/sdk/BApi"; @@ -113,7 +114,12 @@ const PathMarkSettingsButton = ({ + {t("pathMarkConfig.modal.settingsTitle")} + +
+ } visible={visible} onClose={() => setVisible(false)} > diff --git a/src/web/src/pages/path-mark-config/components/PendingSyncListModal.tsx b/src/web/src/pages/path-mark-config/components/PendingSyncListModal.tsx index 011fbd89c..f8120394c 100644 --- a/src/web/src/pages/path-mark-config/components/PendingSyncListModal.tsx +++ b/src/web/src/pages/path-mark-config/components/PendingSyncListModal.tsx @@ -18,6 +18,7 @@ import { import SyncProgressModal from "./SyncProgressModal"; import { Modal, Button, Chip, Spinner, Tooltip, CircularProgress } from "@/components/bakaui"; +import { HelpCenterButton } from "@/components/HelpCenter"; import { PathMarkSyncStatus, PathMarkType, BTaskStatus } from "@/sdk/constants"; import { useBakabaseContext } from "@/components/ContextProvider/BakabaseContextProvider"; import BApi from "@/sdk/BApi"; @@ -279,6 +280,7 @@ const PendingSyncListModal = ({ {totalPending} )} + } visible={isOpen} diff --git a/src/web/src/pages/path-mark-config/index.tsx b/src/web/src/pages/path-mark-config/index.tsx index c3465e093..32a5a7967 100644 --- a/src/web/src/pages/path-mark-config/index.tsx +++ b/src/web/src/pages/path-mark-config/index.tsx @@ -11,10 +11,15 @@ import PathMarkTreeView from "./components/PathMarkTreeView"; import PathMarkSettingsButton from "./components/PathMarkSettingsButton"; import PendingSyncButton from "./components/PendingSyncButton"; import usePathMarks from "./hooks/usePathMarks"; -import { PathMarkGuideModal, usePathMarkGuide } from "./components/PathMarkGuide"; import { Button } from "@/components/bakaui"; import BetaChip from "@/components/Chips/BetaChip"; +import { + HelpCenterButton, + HelpCenterModal, + PATH_MARK_FIRST_RUN_KEY, + useFirstRunHelp, +} from "@/components/HelpCenter"; const PATH_MARK_ROOT_PATH_KEY = "pathMarkConfig.rootPath"; @@ -24,7 +29,7 @@ const PathRuleConfigPage = () => { const [searchParams] = useSearchParams(); const { loadAllMarks } = usePathMarks(); - const { showGuide, completeGuide } = usePathMarkGuide(); + const { showFirstRun, completeFirstRun } = useFirstRunHelp(PATH_MARK_FIRST_RUN_KEY); const [rootPath, setRootPath] = useState(); const [rootPathInitialized, setRootPathInitialized] = useState(false); @@ -68,6 +73,7 @@ const PathRuleConfigPage = () => {

{t("pathMarkConfig.title")}

+
{/* Actions */} @@ -129,7 +135,14 @@ const PathRuleConfigPage = () => { {/* First-time user guide */} - + {showFirstRun && ( + + )} ); }; diff --git a/src/web/src/pages/path-marks/index.tsx b/src/web/src/pages/path-marks/index.tsx index ac811047b..29340d571 100644 --- a/src/web/src/pages/path-marks/index.tsx +++ b/src/web/src/pages/path-marks/index.tsx @@ -18,9 +18,11 @@ import PathMarkSettingsButton from "@/pages/path-mark-config/components/PathMark import PendingSyncButton from "@/pages/path-mark-config/components/PendingSyncButton"; import CopyMarksSidebar from "@/pages/path-mark-config/components/CopyMarksSidebar"; import { - PathMarkGuideModal, - usePathMarkGuide, -} from "@/pages/path-mark-config/components/PathMarkGuide"; + HelpCenterButton, + HelpCenterModal, + PATH_MARK_FIRST_RUN_KEY, + useFirstRunHelp, +} from "@/components/HelpCenter"; import { useBakabaseContext } from "@/components/ContextProvider/BakabaseContextProvider"; import { Button, toast, Modal, Spinner, Switch } from "@/components/bakaui"; import BetaChip from "@/components/Chips/BetaChip"; @@ -30,7 +32,7 @@ const PathMarksPage = () => { const { t } = useTranslation(); const navigate = useNavigate(); const { createPortal } = useBakabaseContext(); - const { showGuide, completeGuide } = usePathMarkGuide(); + const { showFirstRun, completeFirstRun } = useFirstRunHelp(PATH_MARK_FIRST_RUN_KEY); const [showOnlyInvalid, setShowOnlyInvalid] = useState(false); @@ -261,6 +263,7 @@ const PathMarksPage = () => {

{t("pathMarks.title")}

+
{/* Actions */} @@ -345,7 +348,14 @@ const PathMarksPage = () => { {/* First-time user guide */} - + {showFirstRun && ( + + )} ); };