mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 17:11:00 +00:00
feat: initial plugin support
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
useAudio,
|
||||
useGame,
|
||||
} from "../../context/LauncherContext";
|
||||
import { usePluginActions } from "../../plugins/PluginContext";
|
||||
import type { Edition } from "../../types/edition";
|
||||
|
||||
const HomeView = memo(function HomeView() {
|
||||
@@ -23,6 +24,7 @@ const HomeView = memo(function HomeView() {
|
||||
stopGame,
|
||||
updatesAvailable,
|
||||
} = useGame();
|
||||
const pluginActions = usePluginActions("home-menu");
|
||||
|
||||
const isFocusedSection = focusSection === "menu";
|
||||
const selectedEdition = editions.find((e: Edition) => e.id === profile);
|
||||
@@ -34,8 +36,8 @@ const HomeView = memo(function HomeView() {
|
||||
const hasAnyInstall = installs.length > 0;
|
||||
|
||||
const buttonsVal = useMemo(
|
||||
() => [
|
||||
{
|
||||
() => {
|
||||
const mainBtn = {
|
||||
label: !hasAnyInstall
|
||||
? "Install a version"
|
||||
: isGameRunning
|
||||
@@ -56,36 +58,54 @@ const HomeView = memo(function HomeView() {
|
||||
: () => toggleInstall(profile),
|
||||
isDanger: isGameRunning,
|
||||
disabled: isDownloading,
|
||||
},
|
||||
{
|
||||
label: "Help & Options",
|
||||
action: () => setActiveView("settings"),
|
||||
id: "main-action",
|
||||
};
|
||||
|
||||
const pluginBtns = pluginActions.map((a) => ({
|
||||
label: a.label,
|
||||
action: () => a.onClick(),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "settings",
|
||||
},
|
||||
{
|
||||
label: "Versions",
|
||||
action: () => setActiveView("versions"),
|
||||
disabled: false,
|
||||
id: "versions",
|
||||
},
|
||||
{
|
||||
label: "Workshop",
|
||||
action: () => setActiveView("workshop"),
|
||||
disabled: false,
|
||||
id: "workshop",
|
||||
},
|
||||
{
|
||||
label: "Developer Tools",
|
||||
action: () => setActiveView("devtools"),
|
||||
disabled: false,
|
||||
id: "devtools",
|
||||
},
|
||||
],
|
||||
id: a.id,
|
||||
}));
|
||||
|
||||
const menuBtns = [
|
||||
{
|
||||
label: "Help & Options",
|
||||
action: () => setActiveView("settings"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "settings",
|
||||
},
|
||||
{
|
||||
label: "Versions",
|
||||
action: () => setActiveView("versions"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "versions",
|
||||
},
|
||||
{
|
||||
label: "Workshop",
|
||||
action: () => setActiveView("workshop"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "workshop",
|
||||
},
|
||||
{
|
||||
label: "Developer Tools",
|
||||
action: () => setActiveView("devtools"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "devtools",
|
||||
},
|
||||
];
|
||||
|
||||
return [mainBtn, ...pluginBtns, ...menuBtns];
|
||||
},
|
||||
[
|
||||
isDownloading, hasAnyInstall, isInstalled, selectedVersionName,
|
||||
handleLaunch, toggleInstall, profile, setActiveView,
|
||||
isGameRunning, stopGame,
|
||||
isGameRunning, stopGame, pluginActions,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useMemo, memo } from "react";
|
||||
import { useState, useEffect, useRef, useMemo, memo, useCallback } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService, Runner } from "../../services/TauriService";
|
||||
import { usePlatform } from "../../hooks/usePlatform";
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useAudio,
|
||||
useGame,
|
||||
} from "../../context/LauncherContext";
|
||||
import { PluginManager, type PluginInfo } from "../../plugins/PluginManager";
|
||||
|
||||
const SettingsView = memo(function SettingsView() {
|
||||
const { setActiveView } = useUI();
|
||||
@@ -60,9 +61,10 @@ const SettingsView = memo(function SettingsView() {
|
||||
const { isLinux, isMac } = usePlatform();
|
||||
const [focusIndex, setFocusIndex] = useState<number | null>(null);
|
||||
const [currentSubMenu, setCurrentSubMenu] = useState<
|
||||
"main" | "audio" | "video" | "controls" | "launcher" | "game"
|
||||
"main" | "audio" | "video" | "controls" | "launcher" | "game" | "plugins"
|
||||
>("main");
|
||||
const [runners, setRunners] = useState<Runner[]>([]);
|
||||
const [pluginsInfo, setPluginsInfo] = useState<PluginInfo[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [argsInput, setArgsInput] = useState("");
|
||||
const [prefixInput, setPrefixInput] = useState("");
|
||||
@@ -77,6 +79,16 @@ const SettingsView = memo(function SettingsView() {
|
||||
TauriService.getAvailableRunners().then(setRunners);
|
||||
}, [isRunnerDownloading]);
|
||||
|
||||
const refreshPlugins = useCallback(() => {
|
||||
setPluginsInfo(PluginManager.instance.getPluginInfoList());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshPlugins();
|
||||
PluginManager.instance.setEnabledChangedCallback(refreshPlugins);
|
||||
return () => PluginManager.instance.setEnabledChangedCallback(null!);
|
||||
}, [refreshPlugins]);
|
||||
|
||||
const handleLayoutToggle = () => {
|
||||
playPressSound();
|
||||
const currentIndex = layouts.indexOf(layout);
|
||||
@@ -318,6 +330,16 @@ const SettingsView = memo(function SettingsView() {
|
||||
setFocusIndex(0);
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
id: "plugins_menu",
|
||||
label: "Plugins",
|
||||
type: "button",
|
||||
onClick: () => {
|
||||
playPressSound();
|
||||
setCurrentSubMenu("plugins");
|
||||
setFocusIndex(0);
|
||||
},
|
||||
});
|
||||
} else if (currentSubMenu === "audio") {
|
||||
items.push({
|
||||
id: "music",
|
||||
@@ -680,7 +702,9 @@ const SettingsView = memo(function SettingsView() {
|
||||
? "Controls"
|
||||
: currentSubMenu === "game"
|
||||
? "Game"
|
||||
: "Launcher"}
|
||||
: currentSubMenu === "plugins"
|
||||
? "Plugins"
|
||||
: "Launcher"}
|
||||
</h2>
|
||||
|
||||
{currentSubMenu === "main" ? (
|
||||
@@ -750,6 +774,74 @@ const SettingsView = memo(function SettingsView() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : currentSubMenu === "plugins" ? (
|
||||
<div className="min-w-[640px] w-fit p-4 flex flex-col items-center mc-options-bg">
|
||||
<div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2 settings-scrollbar">
|
||||
{pluginsInfo.length === 0 ? (
|
||||
<div className="text-[#888888] text-lg mc-text-shadow py-8">
|
||||
No plugins installed
|
||||
</div>
|
||||
) : (
|
||||
pluginsInfo.map((p, index) => {
|
||||
const isFocused = focusIndex === index;
|
||||
return (
|
||||
<div
|
||||
key={p.manifest.id}
|
||||
data-index={index}
|
||||
onMouseEnter={() => setFocusIndex(index)}
|
||||
className={`w-[600px] flex items-center gap-3 px-4 py-3 cursor-pointer outline-none border-none ${
|
||||
isFocused ? "text-[#ffff00]" : "text-[#333333]"
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background2.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
PluginManager.instance.setPluginEnabled(
|
||||
p.manifest.id,
|
||||
!p.enabled,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="relative w-6 h-6 shrink-0 flex items-center justify-center">
|
||||
<img
|
||||
src={
|
||||
isFocused
|
||||
? "/images/checkbox_highlighted.png"
|
||||
: "/images/checkbox.png"
|
||||
}
|
||||
alt="checkbox"
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
{p.enabled && (
|
||||
<img
|
||||
src="/images/check.png"
|
||||
alt="checked"
|
||||
className="relative z-10 w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<span className="text-lg mc-text-shadow truncate">
|
||||
{p.manifest.name}
|
||||
</span>
|
||||
<span className="text-xs mc-text-shadow opacity-70 truncate">
|
||||
{p.manifest.description}
|
||||
</span>
|
||||
<span className="text-xs mc-text-shadow opacity-50">
|
||||
by {p.manifest.author} · v{p.manifest.version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-[640px] w-fit p-4 flex flex-col items-center mc-options-bg">
|
||||
<div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2 settings-scrollbar">
|
||||
@@ -803,7 +895,7 @@ const SettingsView = memo(function SettingsView() {
|
||||
className={`w-[600px] h-10 flex items-center pl-1.5 pr-4 relative z-30 outline-none border-none shrink-0 rounded ${focusIndex === index ? "text-[#ffff00]" : isRed ? "text-red-600" : "text-[#333333]"}`}
|
||||
>
|
||||
{isToggle && (
|
||||
<div className="relative w-6 h-6 mr-3 shrink-0">
|
||||
<div className="relative w-6 h-6 mr-3 shrink-0 flex items-center justify-center">
|
||||
<img
|
||||
src={
|
||||
focusIndex === index
|
||||
@@ -811,14 +903,14 @@ const SettingsView = memo(function SettingsView() {
|
||||
: "/images/checkbox.png"
|
||||
}
|
||||
alt="checkbox"
|
||||
className="w-full h-full object-contain"
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
{toggleState && (
|
||||
<img
|
||||
src="/images/check.png"
|
||||
alt="checked"
|
||||
className="absolute inset-0 w-full h-full object-contain"
|
||||
className="relative z-10 w-6 h-6 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user