From 368976e9b45f6b4fdb2443dd0f0d8443f8d3d819 Mon Sep 17 00:00:00 2001 From: rhit-daughewd <148395658+rhit-daughewd@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:58:41 -0400 Subject: [PATCH 1/4] Modpacks Implementation --- backend/app/interactions.go | 4 + backend/args.go | 7 +- backend/ficsitcli/modpack.go | 188 ++++++++++++++++++ cspell.json | 1 + frontend/src/App.svelte | 15 ++ .../src/gql/modpacks/modpackSummary.graphql | 9 + .../modals/ExternalInstallModpack.svelte | 82 ++++++++ 7 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 backend/ficsitcli/modpack.go create mode 100644 frontend/src/gql/modpacks/modpackSummary.graphql create mode 100644 frontend/src/lib/components/modals/ExternalInstallModpack.svelte diff --git a/backend/app/interactions.go b/backend/app/interactions.go index 268c7e6e..fa9ca8c7 100644 --- a/backend/app/interactions.go +++ b/backend/app/interactions.go @@ -111,6 +111,10 @@ func (a *app) ExternalImportProfile(path string) { wailsRuntime.EventsEmit(common.AppContext, "externalImportProfile", path) } +func (a *app) ExternalInstallModpack(modpackID, version string) { + wailsRuntime.EventsEmit(common.AppContext, "externalInstallModpack", modpackID, version) +} + func (a *app) Show() { wailsRuntime.WindowUnminimise(common.AppContext) wailsRuntime.Show(common.AppContext) diff --git a/backend/args.go b/backend/args.go index 161f9093..3152489d 100644 --- a/backend/args.go +++ b/backend/args.go @@ -36,8 +36,13 @@ func handleURI(uri string) error { switch u.Host { case "install": modID := u.Query().Get("modID") + modpackID := u.Query().Get("modpackID") version := u.Query().Get("version") - app.App.ExternalInstallMod(modID, version) + if modpackID != "" { + app.App.ExternalInstallModpack(modpackID, version) + } else { + app.App.ExternalInstallMod(modID, version) + } return nil default: return fmt.Errorf("unknown URI action %s", u.Host) diff --git a/backend/ficsitcli/modpack.go b/backend/ficsitcli/modpack.go new file mode 100644 index 00000000..1a5fbb06 --- /dev/null +++ b/backend/ficsitcli/modpack.go @@ -0,0 +1,188 @@ +package ficsitcli + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + + "github.com/spf13/viper" +) + +type ModpackReleaseResponse struct { + GetModpackRelease struct { + Lockfile string `json:"lockfile"` + } `json:"getModpackRelease"` +} + +type PlatformTarget struct { + Hash string `json:"hash"` + Link string `json:"link"` +} + +type ModEntry struct { + Dependencies interface{} `json:"dependencies"` + Targets map[string]PlatformTarget `json:"targets"` + Version string `json:"version"` +} + +type Lockfile struct { + Mods map[string]ModEntry `json:"mods"` + Version int `json:"version"` +} + +func (f *ficsitCLI) InstallModpackRelease(modpackID string, release string, name string) error { + return f.action(ActionInstall, newItem(modpackID, release), func(l *slog.Logger, taskUpdates chan<- taskUpdate) error { + selectedInstallation := f.GetSelectedInstall() + if selectedInstallation == nil { + return fmt.Errorf("no installation selected") + } + + l = l.With( + slog.String("install", selectedInstallation.Path), + slog.String("profile", selectedInstallation.Profile), + ) + f.AddProfile(name + "-" + release) + profileErr := f.setProfileModpack(l, name+"-"+release) + if profileErr != nil { + l.Error("failed to set profile", slog.Any("error", profileErr)) + return fmt.Errorf("failed to set profile: %w", profileErr) + } + + lockfile, err := getLockfile(modpackID, release) + if err != nil { + return fmt.Errorf("failed to get lockfile: %w", err) + } + + for modID, mod := range lockfile.Mods { + modErr := f.installModVersionModpack(l, modID, mod.Version) + if modErr != nil { + l.Error("failed to install mod", + slog.String("mod", modID), + slog.String("version", mod.Version), + slog.Any("error", modErr)) + + return fmt.Errorf("failed to install mod: %s@%s: %w", + modID, mod.Version, modErr) + } + } + + installErr := f.apply(l, taskUpdates) + if installErr != nil { + l.Error("failed to install", slog.Any("error", installErr)) + return installErr + } + + return nil + }) +} + +type graphQLResponse struct { + Data struct { + GetModpackRelease struct { + Lockfile string `json:"lockfile"` + } `json:"getModpackRelease"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` +} + +func getLockfile(modpackID string, release string) (Lockfile, error) { + endpoint := viper.GetString("api-base") + viper.GetString("graphql-api") + + body := map[string]interface{}{ + "query": `query GetModpackRelease($modpackID: ModpackID!, $version: String!) { + getModpackRelease(modpackID: $modpackID, version: $version) { + lockfile + } + }`, + "variables": map[string]interface{}{ + "modpackID": modpackID, + "version": release, + }, + } + + jsonBody, _ := json.Marshal(body) + resp, err := (&http.Client{}).Post(endpoint, "application/json", bytes.NewBuffer(jsonBody)) + if err != nil { + return Lockfile{}, fmt.Errorf("failed to query GraphQL: %w", err) + } + defer resp.Body.Close() + + var gqlResp graphQLResponse + if err := json.NewDecoder(resp.Body).Decode(&gqlResp); err != nil { + return Lockfile{}, fmt.Errorf("failed to decode response: %w", err) + } + + if len(gqlResp.Errors) > 0 { + return Lockfile{}, fmt.Errorf("GraphQL error: %s", gqlResp.Errors[0].Message) + } + if gqlResp.Data.GetModpackRelease.Lockfile == "" { + return Lockfile{}, fmt.Errorf("lockfile not found for %s@%s", modpackID, release) + } + + var lockfile Lockfile + if err := json.Unmarshal([]byte(gqlResp.Data.GetModpackRelease.Lockfile), &lockfile); err != nil { + return Lockfile{}, fmt.Errorf("failed to parse lockfile: %w", err) + } + return lockfile, nil +} + +func (f *ficsitCLI) setProfileModpack(l *slog.Logger, profile string) error { + selectedInstallation := f.GetSelectedInstall() + + if selectedInstallation == nil { + l.Error("no installation selected") + return fmt.Errorf("no installation selected") + } + + if selectedInstallation.Profile == profile { + return nil + } + + err := selectedInstallation.SetProfile(f.ficsitCli, profile) + if err != nil { + l.Error("failed to set profile", slog.Any("error", err)) + return fmt.Errorf("failed to set profile: %w", err) + } + + err = f.ficsitCli.Installations.Save() + if err != nil { + l.Error("failed to save installations", slog.Any("error", err)) + } + + f.EmitGlobals() + f.EmitModsChange() + + return nil +} + +func (f *ficsitCLI) installModVersionModpack(l *slog.Logger, mod string, version string) error { + selectedInstallation := f.GetSelectedInstall() + + if selectedInstallation == nil { + return fmt.Errorf("no installation selected") + } + + l = l.With( + slog.String("install", selectedInstallation.Path), + slog.String("profile", selectedInstallation.Profile), + ) + + profile := f.GetProfile(selectedInstallation.Profile) + + profileErr := profile.AddMod(mod, version) + if profileErr != nil { + l.Error("failed to add mod", slog.Any("error", profileErr)) + return fmt.Errorf("failed to add mod: %s@%s: %w", mod, version, profileErr) + } + + err := f.ficsitCli.Profiles.Save() + if err != nil { + l.Error("failed to save profile", slog.Any("error", err)) + } + + return nil +} diff --git a/cspell.json b/cspell.json index 1042873d..f7c9a421 100644 --- a/cspell.json +++ b/cspell.json @@ -19,6 +19,7 @@ "Maximised", "Minimised", "mircearoata", + "Modpack", "noclose", "Nyan", "smmanager", diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 83089140..0c20bc81 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -13,6 +13,7 @@ import ErrorDetails from '$lib/components/modals/ErrorDetails.svelte'; import ErrorModal from '$lib/components/modals/ErrorModal.svelte'; import ExternalInstallMod from '$lib/components/modals/ExternalInstallMod.svelte'; + import ExternalInstallModpack from '$lib/components/modals/ExternalInstallModpack.svelte'; import MigrationModal from '$lib/components/modals/MigrationModal.svelte'; import { supportedProgressTypes } from '$lib/components/modals/ProgressModal.svelte'; import FirstTimeSetupModal from '$lib/components/modals/first-time-setup/FirstTimeSetupModal.svelte'; @@ -224,6 +225,20 @@ }); }); + EventsOn('externalInstallModpack', (modpackReference: string, version: string) => { + if (!modpackReference) return; + modalStore.trigger({ + type: 'component', + component: { + ref: ExternalInstallModpack, + props: { + modpackReference, + version, + }, + }, + }); + }); + EventsOn('externalImportProfile', async (path: string) => { if (!path) return; modalStore.trigger({ diff --git a/frontend/src/gql/modpacks/modpackSummary.graphql b/frontend/src/gql/modpacks/modpackSummary.graphql new file mode 100644 index 00000000..52691cb6 --- /dev/null +++ b/frontend/src/gql/modpacks/modpackSummary.graphql @@ -0,0 +1,9 @@ +query GetModpackSummary($modpackID: ModpackID!) { + modpack: getModpack(modpackID: $modpackID) { + name + logo + views + short_description + parent_id + } +} \ No newline at end of file diff --git a/frontend/src/lib/components/modals/ExternalInstallModpack.svelte b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte new file mode 100644 index 00000000..86bba10b --- /dev/null +++ b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte @@ -0,0 +1,82 @@ + + +
+
+ +
+
+ {#if modpack} +
+
+

{modpack.name}

+ {#if version} +

+ {:else} +

+ {/if} +

{modpack.short_description}

+
+ +
+ {:else if $modpackQuery.fetching} +

+ {:else if $modpackQuery.error} +

+ {/if} +
+ +
From a1d2755119005ae9da34dd3a10e38f377d1585b4 Mon Sep 17 00:00:00 2001 From: rhit-daughewd <148395658+rhit-daughewd@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:09:05 -0400 Subject: [PATCH 2/4] golangci fixes Co-authored-by: Copilot --- backend/app/debug_info.go | 2 +- backend/ficsitcli/modpack.go | 16 +++++++++++----- .../modals/ExternalInstallModpack.svelte | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/app/debug_info.go b/backend/app/debug_info.go index cc51f463..2a578b72 100644 --- a/backend/app/debug_info.go +++ b/backend/app/debug_info.go @@ -165,7 +165,7 @@ func addMetadata(writer *zip.Writer) error { ficsitCliProfileNames := ficsitcli.FicsitCLI.GetProfiles() selectedMetadataProfileName := ficsitcli.FicsitCLI.GetSelectedProfile() - metadataProfiles := make([]*ficsitCli.Profile, 0) + metadataProfiles := make([]*ficsitCli.Profile, 0, len(ficsitCliProfileNames)) for _, profileName := range ficsitCliProfileNames { p := ficsitcli.FicsitCLI.GetProfile(profileName) diff --git a/backend/ficsitcli/modpack.go b/backend/ficsitcli/modpack.go index 1a5fbb06..6be1ca9f 100644 --- a/backend/ficsitcli/modpack.go +++ b/backend/ficsitcli/modpack.go @@ -43,11 +43,17 @@ func (f *ficsitCLI) InstallModpackRelease(modpackID string, release string, name slog.String("install", selectedInstallation.Path), slog.String("profile", selectedInstallation.Profile), ) - f.AddProfile(name + "-" + release) - profileErr := f.setProfileModpack(l, name+"-"+release) - if profileErr != nil { - l.Error("failed to set profile", slog.Any("error", profileErr)) - return fmt.Errorf("failed to set profile: %w", profileErr) + + addProfileErr := f.AddProfile(name + "-" + release) + if addProfileErr != nil { + l.Error("failed to add profile", slog.Any("error", addProfileErr)) + return fmt.Errorf("failed to add profile: %w", addProfileErr) + } + + setProfileErr := f.setProfileModpack(l, name+"-"+release) + if setProfileErr != nil { + l.Error("failed to set profile", slog.Any("error", setProfileErr)) + return fmt.Errorf("failed to set profile: %w", setProfileErr) } lockfile, err := getLockfile(modpackID, release) diff --git a/frontend/src/lib/components/modals/ExternalInstallModpack.svelte b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte index 86bba10b..6093a72a 100644 --- a/frontend/src/lib/components/modals/ExternalInstallModpack.svelte +++ b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte @@ -30,7 +30,8 @@ function install() { if (!modpack) return; - const action = async () => (InstallModpackRelease(modpackReference, version, modpack.name)).catch((e) => $error = e); + const modpackName = modpack.name; + const action = async () => (InstallModpackRelease(modpackReference, version, modpackName)).catch((e) => $error = e); const actionName = 'install'; addQueuedModAction( modpackReference, From c48c188b228d346db04e7fc1cce7089f01fe8f81 Mon Sep 17 00:00:00 2001 From: rhit-daughewd <148395658+rhit-daughewd@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:39:34 -0400 Subject: [PATCH 3/4] Existing profile check Co-authored-by: Copilot --- .../lib/components/modals/ExternalInstallModpack.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/modals/ExternalInstallModpack.svelte b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte index 6093a72a..a2a878ba 100644 --- a/frontend/src/lib/components/modals/ExternalInstallModpack.svelte +++ b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte @@ -7,6 +7,7 @@ import { error } from '$lib/store/generalStore'; import { offline } from '$lib/store/settingsStore'; import { InstallModpackRelease } from '$wailsjs/go/ficsitcli/ficsitCLI'; + import { profiles } from '$lib/store/ficsitCLIStore'; export let parent: { onClose: () => void }; @@ -27,6 +28,7 @@ ); $: modpack = $modpackQuery.fetching ? null : $modpackQuery.data?.modpack; + $: isInstalled = $profiles?.includes(`${modpack?.name}-${version}`); function install() { if (!modpack) return; @@ -71,8 +73,13 @@