aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/components/config-editor.tsx
blob: 129b8f72f5f4655c60487c1742bd84aff73884bd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import type React from "react";
import { useEffect, useState } from "react";
import { type ZodType, z } from "zod";
import { useSettingsStore } from "@/models/settings";
import type { LauncherConfig } from "@/types";
import { Button } from "./ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "./ui/dialog";
import { FieldError } from "./ui/field";
import { Spinner } from "./ui/spinner";
import { Textarea } from "./ui/textarea";

const launcherConfigSchema: ZodType<LauncherConfig> = z.object({
  minMemory: z.number(),
  maxMemory: z.number(),
  javaPath: z.string(),
  width: z.number(),
  height: z.number(),
  downloadThreads: z.number(),
  customBackgroundPath: z.string().nullable(),
  enableGpuAcceleration: z.boolean(),
  enableVisualEffects: z.boolean(),
  activeEffect: z.string(),
  theme: z.string(),
  logUploadService: z.string(),
  pastebinApiKey: z.string().nullable(),
  assistant: z.any(), // TODO: AssistantConfig schema
  useSharedCaches: z.boolean(),
  keepLegacyPerInstanceStorage: z.boolean(),
  featureFlags: z.any(), // TODO: FeatureFlags schema
});

export interface ConfigEditorProps
  extends Omit<React.ComponentPropsWithoutRef<typeof Dialog>, "onOpenChange"> {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export function ConfigEditor({ onOpenChange, ...props }: ConfigEditorProps) {
  const settings = useSettingsStore();

  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const [rawConfigContent, setRawConfigContent] = useState(
    JSON.stringify(settings.config, null, 2),
  );
  const [isSaving, setIsSaving] = useState(false);

  useEffect(() => {
    setRawConfigContent(JSON.stringify(settings.config, null, 2));
  }, [settings.config]);

  const handleSave = async () => {
    setIsSaving(true);
    setErrorMessage(null);
    try {
      const validatedConfig = launcherConfigSchema.parse(
        JSON.parse(rawConfigContent),
      );
      settings.config = validatedConfig;
      await settings.save();
      onOpenChange?.(false);
    } catch (error) {
      setErrorMessage(error instanceof Error ? error.message : String(error));
    } finally {
      setIsSaving(false);
    }
  };

  return (
    <Dialog onOpenChange={onOpenChange} {...props}>
      <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden">
        <DialogHeader>
          <DialogTitle>Edit Configuration</DialogTitle>
          <DialogDescription>
            Edit the raw JSON configuration file.
          </DialogDescription>
        </DialogHeader>

        <Textarea
          value={rawConfigContent}
          onChange={(e) => setRawConfigContent(e.target.value)}
          className="font-mono text-sm h-100 resize-none"
          spellCheck={false}
          aria-invalid={!!errorMessage}
        />

        {errorMessage && <FieldError errors={[{ message: errorMessage }]} />}

        <DialogFooter>
          <Button
            variant="outline"
            onClick={() => onOpenChange?.(false)}
            disabled={isSaving}
          >
            Cancel
          </Button>
          <Button onClick={handleSave} disabled={isSaving}>
            {isSaving && <Spinner />}
            Save Changes
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}