Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/debug_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions backend/app/interactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion backend/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
194 changes: 194 additions & 0 deletions backend/ficsitcli/modpack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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),
)

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)
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
}
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"Maximised",
"Minimised",
"mircearoata",
"Modpack",
"noclose",
"Nyan",
"smmanager",
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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({
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/gql/modpacks/modpackSummary.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query GetModpackSummary($modpackID: ModpackID!) {
modpack: getModpack(modpackID: $modpackID) {
name
logo
views
short_description
parent_id
}
}
103 changes: 103 additions & 0 deletions frontend/src/lib/components/modals/ExternalInstallModpack.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import { getContextClient, queryStore } from '@urql/svelte';

import T from '$lib/components/T.svelte';
import { GetModpackSummaryDocument } from '$lib/generated';
import { addQueuedModAction } from '$lib/store/actionQueue';
import { error } from '$lib/store/generalStore';
import { offline } from '$lib/store/settingsStore';
import { InstallModpackRelease } from '$wailsjs/go/ficsitcli/ficsitCLI';
import { profiles, selectedProfile } from '$lib/store/ficsitCLIStore';

export let parent: { onClose: () => void };

export let modpackReference: string;
export let version: string;

const client = getContextClient();

$: modpackQuery = queryStore(
{
query: GetModpackSummaryDocument,
client,
pause: !!$offline,
variables: {
modpackID: modpackReference,
},
},
);

$: modpack = $modpackQuery.fetching ? null : $modpackQuery.data?.modpack;
$: isInstalled = $profiles?.includes(`${modpack?.name}-${version}`);

function install() {
if (!modpack) return;
const modpackName = modpack.name;
const action = async () => (InstallModpackRelease(modpackReference, version, modpackName)).catch((e) => $error = e);
const actionName = 'install';
addQueuedModAction(
modpackReference,
actionName,
action,
);
parent.onClose();
}

function switchToProfile() {
if (!modpack) return;
const modpackName = modpack.name;
selectedProfile.asyncSet(`${modpackName}-${version}`);
parent.onClose();
}

$: renderedLogo = modpack?.logo || 'https://ficsit.app/images/no_image.webp';
</script>

<div style="max-height: calc(100vh - 3rem); max-width: calc(100vw - 3rem);" class="w-[48rem] card flex flex-col gap-2">
<header class="card-header font-bold text-2xl text-center">
<T defaultValue="Install modpack" keyName="external-install-modpack.title" />
</header>
<section class="p-4 overflow-y-auto">
{#if modpack}
<div class="flex">
<div class="grow">
<p>{modpack.name}</p>
{#if version}
<p><T defaultValue={'Version {version}'} keyName="external-install-modpack.version" params={{ version }} /></p>
{:else}
<p><T defaultValue="Latest version" keyName="external-install-modpack.latest-version" /></p>
{/if}
<p>{modpack.short_description}</p>
</div>
<img class="logo h-24 w-24 mx-2" alt="{modpack.name} Logo" src={renderedLogo} />
</div>
{:else if $modpackQuery.fetching}
<p><T defaultValue="Loading..." keyName="common.loading" /></p>
{:else if $modpackQuery.error}
<p><T defaultValue="Error loading modpack details" keyName="external-install-modpack.error-loading" /></p>
{/if}
</section>
<footer class="card-footer">
{#if isInstalled}
<p class="text-sm text-red-500">
<T defaultValue="This profile already exists" keyName="external-install-modpack.already-installed-warning" />
</p>
<button
class="btn text-primary-600 variant-ringed"
on:click={switchToProfile}>
<T defaultValue="Switch to profile" keyName="external-install-modpack.already-installed" />
</button>
{:else}
<button
class="btn text-primary-600 variant-ringed"
on:click={install}>
<T defaultValue="Install" keyName="external-install-modpack.install" />
</button>
{/if}
<button
class="btn"
on:click={parent.onClose}>
<T defaultValue="Cancel" keyName="common.cancel" />
</button>
</footer>
</div>
Loading