From 66668d85d603c5841d755a6023aa1925559fc6d4 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Wed, 25 Feb 2026 01:32:51 +0800 Subject: chore(workspace): replace legacy codes --- .../ui/src/components/instance-creation-modal.tsx | 552 +++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 packages/ui/src/components/instance-creation-modal.tsx (limited to 'packages/ui/src/components/instance-creation-modal.tsx') diff --git a/packages/ui/src/components/instance-creation-modal.tsx b/packages/ui/src/components/instance-creation-modal.tsx new file mode 100644 index 0000000..8a2b1b4 --- /dev/null +++ b/packages/ui/src/components/instance-creation-modal.tsx @@ -0,0 +1,552 @@ +import { invoke } from "@tauri-apps/api/core"; +import { Loader2, Search } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useInstancesStore } from "@/models/instances"; +import { useGameStore } from "@/stores/game-store"; +import type { Version } from "@/types/bindings/manifest"; +import type { FabricLoaderEntry } from "../types/bindings/fabric"; +import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge"; +import type { Instance } from "../types/bindings/instance"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function InstanceCreationModal({ open, onOpenChange }: Props) { + const gameStore = useGameStore(); + const instancesStore = useInstancesStore(); + + // Steps: 1 = name, 2 = version, 3 = mod loader + const [step, setStep] = useState(1); + + // Step 1 + const [instanceName, setInstanceName] = useState(""); + + // Step 2 + const [versionSearch, setVersionSearch] = useState(""); + const [versionFilter, setVersionFilter] = useState< + "all" | "release" | "snapshot" + >("release"); + const [selectedVersionUI, setSelectedVersionUI] = useState( + null, + ); + + // Step 3 + const [modLoaderType, setModLoaderType] = useState< + "vanilla" | "fabric" | "forge" + >("vanilla"); + const [fabricLoaders, setFabricLoaders] = useState([]); + const [forgeVersions, setForgeVersions] = useState([]); + const [selectedFabricLoader, setSelectedFabricLoader] = useState(""); + const [selectedForgeLoader, setSelectedForgeLoader] = useState(""); + const [loadingLoaders, setLoadingLoaders] = useState(false); + + const loadModLoaders = useCallback(async () => { + if (!selectedVersionUI) return; + setLoadingLoaders(true); + setFabricLoaders([]); + setForgeVersions([]); + try { + if (modLoaderType === "fabric") { + const loaders = await invoke( + "get_fabric_loaders_for_version", + { + gameVersion: selectedVersionUI.id, + }, + ); + setFabricLoaders(loaders || []); + if (loaders && loaders.length > 0) { + setSelectedFabricLoader(loaders[0].loader.version); + } else { + setSelectedFabricLoader(""); + } + } else if (modLoaderType === "forge") { + const versions = await invoke( + "get_forge_versions_for_game", + { + gameVersion: selectedVersionUI.id, + }, + ); + setForgeVersions(versions || []); + if (versions && versions.length > 0) { + // Binding `ForgeVersion` uses `version` (not `id`) — use `.version` here. + setSelectedForgeLoader(versions[0].version); + } else { + setSelectedForgeLoader(""); + } + } + } catch (e) { + console.error("Failed to load mod loaders:", e); + toast.error("Failed to fetch mod loader versions"); + } finally { + setLoadingLoaders(false); + } + }, [modLoaderType, selectedVersionUI]); + + // When entering step 3 and a base version exists, fetch loaders if needed + useEffect(() => { + if (step === 3 && modLoaderType !== "vanilla" && selectedVersionUI) { + loadModLoaders(); + } + }, [step, modLoaderType, selectedVersionUI, loadModLoaders]); + + // Creating state + const [creating, setCreating] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + // Derived filtered versions + const filteredVersions = useMemo(() => { + const all = gameStore.versions || []; + let list = all.slice(); + if (versionFilter !== "all") { + list = list.filter((v) => v.type === versionFilter); + } + if (versionSearch.trim()) { + const q = versionSearch.trim().toLowerCase().replace(/。/g, "."); + list = list.filter((v) => v.id.toLowerCase().includes(q)); + } + return list; + }, [gameStore.versions, versionFilter, versionSearch]); + + // Reset when opened/closed + useEffect(() => { + if (open) { + // ensure versions are loaded + gameStore.loadVersions(); + setStep(1); + setInstanceName(""); + setVersionSearch(""); + setVersionFilter("release"); + setSelectedVersionUI(null); + setModLoaderType("vanilla"); + setFabricLoaders([]); + setForgeVersions([]); + setSelectedFabricLoader(""); + setSelectedForgeLoader(""); + setErrorMessage(""); + setCreating(false); + } + }, [open, gameStore.loadVersions]); + + function validateStep1(): boolean { + if (!instanceName.trim()) { + setErrorMessage("Please enter an instance name"); + return false; + } + setErrorMessage(""); + return true; + } + + function validateStep2(): boolean { + if (!selectedVersionUI) { + setErrorMessage("Please select a Minecraft version"); + return false; + } + setErrorMessage(""); + return true; + } + + async function handleNext() { + setErrorMessage(""); + if (step === 1) { + if (!validateStep1()) return; + setStep(2); + } else if (step === 2) { + if (!validateStep2()) return; + setStep(3); + } + } + + function handleBack() { + setErrorMessage(""); + setStep((s) => Math.max(1, s - 1)); + } + + async function handleCreate() { + if (!validateStep1() || !validateStep2()) return; + setCreating(true); + setErrorMessage(""); + + try { + // Step 1: create instance + const instance = await invoke("create_instance", { + name: instanceName.trim(), + }); + + // If selectedVersion provided, install it + if (selectedVersionUI) { + try { + await invoke("install_version", { + instanceId: instance.id, + versionId: selectedVersionUI.id, + }); + } catch (err) { + console.error("Failed to install base version:", err); + // continue - instance created but version install failed + toast.error( + `Failed to install version ${selectedVersionUI.id}: ${String(err)}`, + ); + } + } + + // If mod loader selected, install it + if (modLoaderType === "fabric" && selectedFabricLoader) { + try { + await invoke("install_fabric", { + instanceId: instance.id, + gameVersion: selectedVersionUI?.id ?? "", + loaderVersion: selectedFabricLoader, + }); + } catch (err) { + console.error("Failed to install Fabric:", err); + toast.error(`Failed to install Fabric: ${String(err)}`); + } + } else if (modLoaderType === "forge" && selectedForgeLoader) { + try { + await invoke("install_forge", { + instanceId: instance.id, + gameVersion: selectedVersionUI?.id ?? "", + installerVersion: selectedForgeLoader, + }); + } catch (err) { + console.error("Failed to install Forge:", err); + toast.error(`Failed to install Forge: ${String(err)}`); + } + } + + // Refresh instances list + await instancesStore.refresh(); + + toast.success("Instance created successfully"); + onOpenChange(false); + } catch (e) { + console.error("Failed to create instance:", e); + setErrorMessage(String(e)); + toast.error(`Failed to create instance: ${e}`); + } finally { + setCreating(false); + } + } + + // UI pieces + const StepIndicator = () => ( +
+
= 1 ? "bg-indigo-500" : "bg-zinc-700"}`} + /> +
= 2 ? "bg-indigo-500" : "bg-zinc-700"}`} + /> +
= 3 ? "bg-indigo-500" : "bg-zinc-700"}`} + /> +
+ ); + + return ( + + + + Create New Instance + + Multi-step wizard — create an instance and optionally install a + version or mod loader. + + + +
+
+ +
+ + {/* Step 1 - Name */} + {step === 1 && ( +
+
+ + setInstanceName(e.target.value)} + disabled={creating} + /> +
+

+ Give your instance a memorable name. +

+
+ )} + + {/* Step 2 - Version selection */} + {step === 2 && ( +
+
+
+ + setVersionSearch(e.target.value)} + placeholder="Search versions..." + className="pl-9" + /> +
+ +
+ + + +
+
+ + +
+ {gameStore.versions.length === 0 ? ( +
+ + Loading versions... +
+ ) : filteredVersions.length === 0 ? ( +
+ No matching versions found +
+ ) : ( + filteredVersions.map((v) => { + const isSelected = selectedVersionUI?.id === v.id; + return ( + + ); + }) + )} +
+
+
+ )} + + {/* Step 3 - Mod loader */} + {step === 3 && ( +
+
+
Mod Loader Type
+
+ + + +
+
+ + {modLoaderType === "fabric" && ( +
+ {loadingLoaders ? ( +
+ + Loading Fabric versions... +
+ ) : fabricLoaders.length > 0 ? ( +
+ +
+ ) : ( +

+ No Fabric loaders available for this version +

+ )} +
+ )} + + {modLoaderType === "forge" && ( +
+ {loadingLoaders ? ( +
+ + Loading Forge versions... +
+ ) : forgeVersions.length > 0 ? ( +
+ +
+ ) : ( +

+ No Forge versions available for this version +

+ )} +
+ )} +
+ )} + + {errorMessage && ( +
{errorMessage}
+ )} +
+ + +
+
+ +
+ +
+ {step > 1 && ( + + )} + + {step < 3 ? ( + + ) : ( + + )} +
+
+
+
+
+ ); +} + +export default InstanceCreationModal; -- cgit v1.2.3-70-g09d2 From d95ca2801c19a89a2a845f43b6e0133bf4e9be50 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Thu, 26 Feb 2026 18:30:57 +0800 Subject: refactor: migrate some invokes --- .changes/migrate-apis.md | 5 + .../ui/src/components/instance-creation-modal.tsx | 70 +++++------ .../ui/src/components/instance-editor-modal.tsx | 4 +- packages/ui/src/models/instance.ts | 131 ++++++++++++++++++++ packages/ui/src/models/instances.ts | 135 --------------------- packages/ui/src/pages/index.tsx | 5 +- packages/ui/src/pages/instances-view.tsx | 4 +- 7 files changed, 175 insertions(+), 179 deletions(-) create mode 100644 .changes/migrate-apis.md create mode 100644 packages/ui/src/models/instance.ts delete mode 100644 packages/ui/src/models/instances.ts (limited to 'packages/ui/src/components/instance-creation-modal.tsx') diff --git a/.changes/migrate-apis.md b/.changes/migrate-apis.md new file mode 100644 index 0000000..54f1566 --- /dev/null +++ b/.changes/migrate-apis.md @@ -0,0 +1,5 @@ +--- +"@dropout/ui": "patch:refactor" +--- + +Migrate tauri invokes of instance creation modal to generated client. diff --git a/packages/ui/src/components/instance-creation-modal.tsx b/packages/ui/src/components/instance-creation-modal.tsx index 8a2b1b4..7c46d0f 100644 --- a/packages/ui/src/components/instance-creation-modal.tsx +++ b/packages/ui/src/components/instance-creation-modal.tsx @@ -1,7 +1,13 @@ -import { invoke } from "@tauri-apps/api/core"; import { Loader2, Search } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { + getFabricLoadersForVersion, + getForgeVersionsForGame, + installFabric, + installForge, + installVersion, +} from "@/client"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -13,12 +19,13 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useInstancesStore } from "@/models/instances"; +import { useInstanceStore } from "@/models/instance"; import { useGameStore } from "@/stores/game-store"; -import type { Version } from "@/types/bindings/manifest"; -import type { FabricLoaderEntry } from "../types/bindings/fabric"; -import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge"; -import type { Instance } from "../types/bindings/instance"; +import type { + FabricLoaderEntry, + ForgeVersion as ForgeVersionEntry, + Version, +} from "@/types"; interface Props { open: boolean; @@ -27,7 +34,7 @@ interface Props { export function InstanceCreationModal({ open, onOpenChange }: Props) { const gameStore = useGameStore(); - const instancesStore = useInstancesStore(); + const instancesStore = useInstanceStore(); // Steps: 1 = name, 2 = version, 3 = mod loader const [step, setStep] = useState(1); @@ -61,12 +68,7 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) { setForgeVersions([]); try { if (modLoaderType === "fabric") { - const loaders = await invoke( - "get_fabric_loaders_for_version", - { - gameVersion: selectedVersionUI.id, - }, - ); + const loaders = await getFabricLoadersForVersion(selectedVersionUI.id); setFabricLoaders(loaders || []); if (loaders && loaders.length > 0) { setSelectedFabricLoader(loaders[0].loader.version); @@ -74,12 +76,7 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) { setSelectedFabricLoader(""); } } else if (modLoaderType === "forge") { - const versions = await invoke( - "get_forge_versions_for_game", - { - gameVersion: selectedVersionUI.id, - }, - ); + const versions = await getForgeVersionsForGame(selectedVersionUI.id); setForgeVersions(versions || []); if (versions && versions.length > 0) { // Binding `ForgeVersion` uses `version` (not `id`) — use `.version` here. @@ -182,17 +179,12 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) { try { // Step 1: create instance - const instance = await invoke("create_instance", { - name: instanceName.trim(), - }); + const instance = await instancesStore.create(instanceName.trim()); // If selectedVersion provided, install it - if (selectedVersionUI) { + if (selectedVersionUI && instance) { try { - await invoke("install_version", { - instanceId: instance.id, - versionId: selectedVersionUI.id, - }); + await installVersion(instance?.id, selectedVersionUI.id); } catch (err) { console.error("Failed to install base version:", err); // continue - instance created but version install failed @@ -203,24 +195,24 @@ export function InstanceCreationModal({ open, onOpenChange }: Props) { } // If mod loader selected, install it - if (modLoaderType === "fabric" && selectedFabricLoader) { + if (modLoaderType === "fabric" && selectedFabricLoader && instance) { try { - await invoke("install_fabric", { - instanceId: instance.id, - gameVersion: selectedVersionUI?.id ?? "", - loaderVersion: selectedFabricLoader, - }); + await installFabric( + instance?.id, + selectedVersionUI?.id ?? "", + selectedFabricLoader, + ); } catch (err) { console.error("Failed to install Fabric:", err); toast.error(`Failed to install Fabric: ${String(err)}`); } - } else if (modLoaderType === "forge" && selectedForgeLoader) { + } else if (modLoaderType === "forge" && selectedForgeLoader && instance) { try { - await invoke("install_forge", { - instanceId: instance.id, - gameVersion: selectedVersionUI?.id ?? "", - installerVersion: selectedForgeLoader, - }); + await installForge( + instance?.id, + selectedVersionUI?.id ?? "", + selectedForgeLoader, + ); } catch (err) { console.error("Failed to install Forge:", err); toast.error(`Failed to install Forge: ${String(err)}`); diff --git a/packages/ui/src/components/instance-editor-modal.tsx b/packages/ui/src/components/instance-editor-modal.tsx index f880c20..d964185 100644 --- a/packages/ui/src/components/instance-editor-modal.tsx +++ b/packages/ui/src/components/instance-editor-modal.tsx @@ -16,7 +16,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/models/instances"; +import { useInstanceStore } from "@/models/instance"; import { useSettingsStore } from "@/models/settings"; import type { FileInfo } from "../types/bindings/core"; import type { Instance } from "../types/bindings/instance"; @@ -28,7 +28,7 @@ type Props = { }; export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { - const instancesStore = useInstancesStore(); + const instancesStore = useInstanceStore(); const { config } = useSettingsStore(); const [activeTab, setActiveTab] = useState< diff --git a/packages/ui/src/models/instance.ts b/packages/ui/src/models/instance.ts new file mode 100644 index 0000000..a3fda3d --- /dev/null +++ b/packages/ui/src/models/instance.ts @@ -0,0 +1,131 @@ +import { toast } from "sonner"; +import { create } from "zustand"; +import { + createInstance, + deleteInstance, + duplicateInstance, + getActiveInstance, + getInstance, + listInstances, + setActiveInstance, + updateInstance, +} from "@/client"; +import type { Instance } from "@/types"; + +interface InstanceState { + instances: Instance[]; + activeInstance: Instance | null; + + refresh: () => Promise; + create: (name: string) => Promise; + delete: (id: string) => Promise; + update: (instance: Instance) => Promise; + setActiveInstance: (instance: Instance) => Promise; + duplicate: (id: string, newName: string) => Promise; + get: (id: string) => Promise; +} + +export const useInstanceStore = create((set, get) => ({ + instances: [], + activeInstance: null, + + refresh: async () => { + const { setActiveInstance } = get(); + try { + const instances = await listInstances(); + const activeInstance = await getActiveInstance(); + + if (!activeInstance && instances.length > 0) { + // If no active instance but instances exist, set the first one as active + await setActiveInstance(instances[0]); + } + + set({ instances, activeInstance }); + } catch (e) { + console.error("Failed to load instances:", e); + toast.error("Error loading instances"); + } + }, + + create: async (name) => { + const { refresh } = get(); + try { + const instance = await createInstance(name); + await refresh(); + toast.success(`Instance "${name}" created successfully`); + return instance; + } catch (e) { + console.error("Failed to create instance:", e); + toast.error("Error creating instance"); + return null; + } + }, + + delete: async (id) => { + const { refresh, instances, activeInstance, setActiveInstance } = get(); + try { + await deleteInstance(id); + await refresh(); + + // If deleted instance was active, set another as active + if (activeInstance?.id === id) { + if (instances.length > 0) { + await setActiveInstance(instances[0]); + } else { + set({ activeInstance: null }); + } + } + + toast.success("Instance deleted successfully"); + } catch (e) { + console.error("Failed to delete instance:", e); + toast.error("Error deleting instance"); + } + }, + + update: async (instance) => { + const { refresh } = get(); + try { + await updateInstance(instance); + await refresh(); + toast.success("Instance updated successfully"); + } catch (e) { + console.error("Failed to update instance:", e); + toast.error("Error updating instance"); + } + }, + + setActiveInstance: async (instance) => { + try { + await setActiveInstance(instance.id); + set({ activeInstance: instance }); + toast.success("Active instance changed"); + } catch (e) { + console.error("Failed to set active instance:", e); + toast.error("Error setting active instance"); + } + }, + + duplicate: async (id, newName) => { + const { refresh } = get(); + try { + const instance = await duplicateInstance(id, newName); + await refresh(); + toast.success(`Instance duplicated as "${newName}"`); + return instance; + } catch (e) { + console.error("Failed to duplicate instance:", e); + toast.error("Error duplicating instance"); + return null; + } + }, + + get: async (id) => { + try { + return await getInstance(id); + } catch (e) { + console.error("Failed to get instance:", e); + return null; + } + }, +})); diff --git a/packages/ui/src/models/instances.ts b/packages/ui/src/models/instances.ts deleted file mode 100644 index f434c7c..0000000 --- a/packages/ui/src/models/instances.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { toast } from "sonner"; -import { create } from "zustand"; -import { - createInstance, - deleteInstance, - duplicateInstance, - getActiveInstance, - getInstance, - listInstances, - setActiveInstance, - updateInstance, -} from "@/client"; -import type { Instance } from "@/types"; - -interface InstancesState { - // State - instances: Instance[]; - activeInstance: Instance | null; - - // Actions - refresh: () => Promise; - create: (name: string) => Promise; - delete: (id: string) => Promise; - update: (instance: Instance) => Promise; - setActiveInstance: (instance: Instance) => Promise; - duplicate: (id: string, newName: string) => Promise; - getInstance: (id: string) => Promise; -} - -export const useInstancesStore = create((set, get) => ({ - // Initial state - instances: [], - activeInstance: null, - - // Actions - refresh: async () => { - const { setActiveInstance } = get(); - try { - const instances = await listInstances(); - const active = await getActiveInstance(); - - if (!active && instances.length > 0) { - // If no active instance but instances exist, set the first one as active - await setActiveInstance(instances[0]); - } - - set({ instances }); - } catch (e) { - console.error("Failed to load instances:", e); - toast.error("Error loading instances"); - } - }, - - create: async (name) => { - const { refresh } = get(); - try { - const instance = await createInstance(name); - await refresh(); - toast.success(`Instance "${name}" created successfully`); - return instance; - } catch (e) { - console.error("Failed to create instance:", e); - toast.error("Error creating instance"); - return null; - } - }, - - delete: async (id) => { - const { refresh, instances, activeInstance, setActiveInstance } = get(); - try { - await deleteInstance(id); - await refresh(); - - // If deleted instance was active, set another as active - if (activeInstance?.id === id) { - if (instances.length > 0) { - await setActiveInstance(instances[0]); - } else { - set({ activeInstance: null }); - } - } - - toast.success("Instance deleted successfully"); - } catch (e) { - console.error("Failed to delete instance:", e); - toast.error("Error deleting instance"); - } - }, - - update: async (instance) => { - const { refresh } = get(); - try { - await updateInstance(instance); - await refresh(); - toast.success("Instance updated successfully"); - } catch (e) { - console.error("Failed to update instance:", e); - toast.error("Error updating instance"); - } - }, - - setActiveInstance: async (instance) => { - try { - await setActiveInstance(instance.id); - set({ activeInstance: instance }); - toast.success("Active instance changed"); - } catch (e) { - console.error("Failed to set active instance:", e); - toast.error("Error setting active instance"); - } - }, - - duplicate: async (id, newName) => { - const { refresh } = get(); - try { - const instance = await duplicateInstance(id, newName); - await refresh(); - toast.success(`Instance duplicated as "${newName}"`); - return instance; - } catch (e) { - console.error("Failed to duplicate instance:", e); - toast.error("Error duplicating instance"); - return null; - } - }, - - getInstance: async (id) => { - try { - return await getInstance(id); - } catch (e) { - console.error("Failed to get instance:", e); - return null; - } - }, -})); diff --git a/packages/ui/src/pages/index.tsx b/packages/ui/src/pages/index.tsx index 54cfc1e..093ccb2 100644 --- a/packages/ui/src/pages/index.tsx +++ b/packages/ui/src/pages/index.tsx @@ -3,18 +3,21 @@ import { Outlet, useLocation } from "react-router"; import { ParticleBackground } from "@/components/particle-background"; import { Sidebar } from "@/components/sidebar"; import { useAuthStore } from "@/models/auth"; +import { useInstanceStore } from "@/models/instance"; import { useSettingsStore } from "@/models/settings"; export function IndexPage() { const authStore = useAuthStore(); const settingsStore = useSettingsStore(); + const instanceStore = useInstanceStore(); const location = useLocation(); useEffect(() => { authStore.init(); settingsStore.refresh(); - }, [authStore.init, settingsStore.refresh]); + instanceStore.refresh(); + }, [authStore.init, settingsStore.refresh, instanceStore.refresh]); return (
diff --git a/packages/ui/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx index ad6bd38..1634905 100644 --- a/packages/ui/src/pages/instances-view.tsx +++ b/packages/ui/src/pages/instances-view.tsx @@ -13,11 +13,11 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/models/instances"; +import { useInstanceStore } from "@/models/instance"; import type { Instance } from "../types/bindings/instance"; export function InstancesView() { - const instancesStore = useInstancesStore(); + const instancesStore = useInstanceStore(); // Modal / UI state const [showCreateModal, setShowCreateModal] = useState(false); -- cgit v1.2.3-70-g09d2