From 2e8e6cdec315644218f741d24f1104773315d6e3 Mon Sep 17 00:00:00 2001 From: /home/neo <158327205+neoapps-dev@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:25:21 +0300 Subject: [PATCH] feat(devtools): community guides (#109) --- public/images/tools/guides.png | Bin 0 -> 8349 bytes src/components/views/DevtoolsView.tsx | 1 + src/components/views/GuidesView.tsx | 284 ++++++++++++++++++++++++++ src/components/views/SkinsView.tsx | 3 +- src/pages/App.tsx | 4 + 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 public/images/tools/guides.png create mode 100644 src/components/views/GuidesView.tsx diff --git a/public/images/tools/guides.png b/public/images/tools/guides.png new file mode 100644 index 0000000000000000000000000000000000000000..69c8a62b1d300a725f44a0c7da67779f0a7baf9e GIT binary patch literal 8349 zcmeHLdrVVT7{3^FfB`hdEjp09nvGOxFOM;7rBt+2X{2+7G29S$+j}XM_7;0@ZGp_9 zb2{P{Hm$PFB7Z2s#Rr=&hWH?HOx}bTV-x|ILKRUI1msb=b6Nxx#I|05_xW;;+EXg_f>bjvTM`xd*2d zk}-IA$&S`c28Zsjz$o#OYq}zS#y{`km-ra*mx#BdJSK$QFC!ZBO|#6bt+hm zAmKtoDlShU8B|6pl$5}~v*e;bsw4#dpN2*UMyIYIla;H}wPZ}XBvziDBIgnQ!U#`~ za04H_K}|9^YEY{vnr{%W>@VhnJ{-na5#d@Qi7yqc99aRL1gvC+N#$dhUa#lsL%15P z0t@2tcvxUC797k0D>!r-#oz`GMK2tAfrzB#T4gGu)KDmVAuiMC7y(PCQxg0nJSkL0 z5TP6y9zt+}f(V?06CptyA|!+e3k_NloD?KujhH9oqt;V(T9w^4A;(A+sU|6g#)7y( z5Hlg?GfGB9I?zX|m={B@Qo`@^RXC*(unZi6Ou}_4h7}QsMz4sHYbY9xq-l}~$KX#> zDI**mU>5k;-Q7JL8^Z&7axIB78m$9c47uq=u|=rmN(h20m#auTi3{ivfT}bE$yF$x zg9NONgER^RfTDG>jij7$fLe|#UIM7+umB_Du#DQb`*0jx9EipZynOy(|F2Ebpd8Z- z2$Ms)AW9gV0hA4dl!bl%Cp4*Dp;1FFJ21R7>c%E3X;i7k6(p+1X;eW{q!wpL0>v2= z%9{rIp2#cA(<2%uNK}?Kx=5j9l65jjIxIz})lg)LhDv*%LUD>1T{A*P>^TOE6UfA0 zKamIa5s-<1yhigDU=aFWud$>_QXo_6K}yX2@qiFP=9fo_B4Z7S4{iuj60+v)XvvLr zpP%6RN8Gj_VpZ&`yQ5O@u!rBJ6h9HVElSv-ldDb-F5IVj;C<#);CtJDJrcR&-7NI$ zd3zSE87P{0=*HmL%7O*HxpeoIp1Jm9{kk@#wreskq_nX4dfCRMVTYe~`(10=y7f?V zwF~gb)1KMTFX%GZW+HG1nFgyS;x-Gm&WTBINoJW3lDwoOUTs-<*_yGF7WcQ^Z{+3Q zq&}d%eBz0G%youjim>!e8sXP+;~I+VH`$t6%+d?}>3RvfXn&2_?fQDI z#U!;^FMeqh6qs_dg#$BKRd9ESxVpi177E*CnXpA%roq-Z0lhFOE-96HoieXKdWJrD zd-(h%UjEP)i=N*0ZBb{TMO1$8@vvFCIj3qwd%p;6i4nth-wv#Hh7ZJNDiF8yuMXMi z+=ae_Z!D$7JV8_5oOd+M8|v6)op~n)nvLtN{Y51Swd~f-Cx40&Tl@RE9$zlIDNdL) zdSyaOPF4S7q6>gYlee$KucMmX`ov(AuuH01%~C3CKW}zdueHZ4ZLbsFXgAt)J(dkt zYiC2}7BdLsV4YKefLBvTf$|E+My0BGIHJIG<+hcV-@(nH#l?-PHc@#)-!)ncvS;!U zn{BYS#1Ev_7NH-Ac-gMEb`|ga9csk6*_;B~?09=A3@A2srb^hxrw{VlOxpgIdb3oL z-DfeKc$P}%5JUH?E?P`oR&QZV*7yFa>cHe@$J$IM5(~lTqn1m~Bs(d)mrSUfe=RQo zlG?sAbO7&chC;%wTX60`Ol3_?jakYShW{K>Ikw3e>hUWAhmGN#c|fy!4q9mOx?^!P zi#VTH$8NoP^$O55%+qDg+zXsOC33IKE7aeT(NMhCr}2D9fl1*FBh}WHBw^?HEvR9) z4p=anb*G}fo0oqy?g($TL!a&K_snknLsiB$(Kj<8ft+W1=!I8Xfxw|Zna68xs6BS) z?!#POet53axghAD&frXq;6O>e#+U7+ ZZCXKBrO)D9yK|r$qCS*}jxYaW+ux8dmWu!Y literal 0 HcmV?d00001 diff --git a/src/components/views/DevtoolsView.tsx b/src/components/views/DevtoolsView.tsx index 00b4fe7..0b9956f 100644 --- a/src/components/views/DevtoolsView.tsx +++ b/src/components/views/DevtoolsView.tsx @@ -10,6 +10,7 @@ interface DevTool { } const DEV_TOOLS: DevTool[] = [ + { id: "guides", name: "Community Guides", view: "guides", comingSoon: false }, { id: "pck", name: "PCK Editor", view: "pck-editor", comingSoon: false }, { id: "arc", name: "ARC Editor", view: "arc-editor", comingSoon: false }, { id: "loc", name: "LOC Editor", view: "loc-editor", comingSoon: false }, diff --git a/src/components/views/GuidesView.tsx b/src/components/views/GuidesView.tsx new file mode 100644 index 0000000..8291f46 --- /dev/null +++ b/src/components/views/GuidesView.tsx @@ -0,0 +1,284 @@ +import { useState, useEffect, useRef, useCallback } from "react"; +import { motion } from "framer-motion"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeRaw from "rehype-raw"; +import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; +import { TauriService } from "../../services/TauriService"; +const API_BASE = "https://api.github.com/repos/LCE-Hub/Guides/contents"; +const RAW_BASE = "https://raw.githubusercontent.com/LCE-Hub/Guides/main"; +interface GuideEntry { + name: string; + path: string; + type: "file" | "dir"; + download_url: string | null; +} + +export default function GuidesView() { + const { setActiveView } = useUI(); + const { playPressSound, playBackSound } = useAudio(); + const { animationsEnabled } = useConfig(); + const [guides, setGuides] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedGuide, setSelectedGuide] = useState(null); + const [guideContent, setGuideContent] = useState(null); + const [contentLoading, setContentLoading] = useState(false); + const [focusIndex, setFocusIndex] = useState(0); + const containerRef = useRef(null); + const BACK_BUTTON_INDEX = guides.length; + useEffect(() => { + let cancelled = false; + setLoading(true); + fetch(API_BASE, { + headers: { Accept: "application/vnd.github.v3+json" }, + }) + .then((res) => { + if (!res.ok) throw new Error(`GitHub API error: ${res.status}`); + return res.json(); + }) + .then((data: GuideEntry[]) => { + if (!cancelled) { + const items = data.filter((e) => e.name.endsWith(".md")); + items.sort((a, b) => a.name.localeCompare(b.name)); + setGuides(items); + setError(null); + } + }) + .catch((err) => { + if (!cancelled) setError(err.message); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + return () => { + cancelled = true; + }; + }, []); + + const fetchContent = useCallback(async (guide: GuideEntry) => { + if (guide.type === "dir") return; + setContentLoading(true); + setGuideContent(null); + setSelectedGuide(guide); + try { + const url = `${RAW_BASE}/${guide.path}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch guide: ${res.status}`); + const text = await res.text(); + setGuideContent(text); + } catch (err: any) { + setGuideContent(`**Error:** ${err.message}`); + } finally { + setContentLoading(false); + } + }, []); + + const goBack = useCallback(() => { + if (selectedGuide) { + setSelectedGuide(null); + setGuideContent(null); + } else { + playBackSound(); + setActiveView("devtools"); + } + }, [selectedGuide, playBackSound, setActiveView]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" || e.key === "Backspace") { + e.preventDefault(); + goBack(); + return; + } + if (selectedGuide) return; + if (e.key === "ArrowDown") { + setFocusIndex((p) => (p >= BACK_BUTTON_INDEX ? 0 : p + 1)); + } else if (e.key === "ArrowUp") { + setFocusIndex((p) => (p <= 0 ? BACK_BUTTON_INDEX : p - 1)); + } else if (e.key === "Enter") { + if (focusIndex === BACK_BUTTON_INDEX) { + goBack(); + } else { + playPressSound(); + fetchContent(guides[focusIndex]); + } + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + focusIndex, + guides, + selectedGuide, + goBack, + playPressSound, + BACK_BUTTON_INDEX, + ]); + + useEffect(() => { + const el = containerRef.current?.querySelector( + `[data-index="${focusIndex}"]`, + ) as HTMLElement; + if (el) el.focus(); + }, [focusIndex]); + + const renderContent = () => { + if (!selectedGuide) return null; + if (contentLoading) { + return ( +
+ + Loading guide... + +
+ ); + } + if (guideContent === null) return null; + return ( +
+ +

+ {selectedGuide.name.replace(/\.md$/i, "")} +

+ +
+ ); + }; + + return ( + +

+ {selectedGuide + ? selectedGuide.name.replace(/\.md$/i, "") + : "Community Guides"} +

+ +
+ {selectedGuide ? ( + renderContent() + ) : loading ? ( +
+ + Loading guides... + +
+ ) : error ? ( +
+ + Failed to load guides + + + {error} + +
+ ) : guides.length === 0 ? ( +
+ + No guides found + +
+ ) : ( +
+ {guides.map((guide, i) => ( +
setFocusIndex(i)} + onClick={() => { + if (guide.type === "dir") return; + playPressSound(); + fetchContent(guide); + }} + className={`flex flex-col items-center gap-3 p-5 cursor-pointer transition-all outline-none border-2 ${ + focusIndex === i + ? "border-[#FFFF55] bg-white/5" + : "border-[#373737] bg-black/20 hover:border-[#555]" + } ${guide.type === "dir" ? "opacity-60 pointer-events-none" : ""}`} + > +
+ +
+ + {guide.name.replace(/\.md$/i, "")} + +
+ ))} +
+ )} +
+ + +
+ ); +} diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index 79e089d..4cf2297 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -60,7 +60,7 @@ const DEFAULT_SKINS: SavedSkin[] = [ ]; const SkinsView = memo(function SkinsView() { - const { setActiveView } = useUI(); + const { setActiveView, setIsUiHidden } = useUI(); const { playPressSound, playBackSound } = useAudio(); const { skinUrl, setSkinUrl, setSkinIsSlim, capeUrl, setCapeUrl } = useSkin(); @@ -712,6 +712,7 @@ const SkinsView = memo(function SkinsView() { setSkinUrl={setSkinUrl} capeUrl={capeUrl} setActiveView={setActiveView} + setIsUiHidden={setIsUiHidden} isFocusedSection={false} onNavigateRight={() => {}} hideControls diff --git a/src/pages/App.tsx b/src/pages/App.tsx index cd49884..05c25e1 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -5,6 +5,7 @@ import HomeView from "../components/views/HomeView"; import SettingsView from "../components/views/SettingsView"; import VersionsView from "../components/views/VersionsView"; import DevtoolsView from "../components/views/DevtoolsView"; +import GuidesView from "../components/views/GuidesView"; import SkinsView from "../components/views/SkinsView"; import WorkshopView from "../components/views/WorkshopView"; import SetupView from "../components/views/SetupView"; @@ -585,6 +586,9 @@ export default function App() { {activeView === "devtools" && ( )} + {activeView === "guides" && ( + + )} {activeView === "pck-editor" && ( )}