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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Export modpack to file via native save dialog, alongside the existing clipboard export.
### Changed
- Moved away from using Radix UI to Base UI for all components in the UI stack.
- Revamp on the whole UI after introducing Base UI.

### Changed

Expand Down
4 changes: 3 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod modules;
use modules::{auth, download, installations, maps, mods, news, saves, servers, versions};
use modules::{auth, download, installations, maps, mods, news, saves, servers, utils, versions};
use tauri::Manager;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
Expand Down Expand Up @@ -81,6 +81,8 @@ pub fn run() {
maps::get_map_bounds,
maps::get_map_tile,
maps::get_all_map_tiles,
// Utils
utils::save_file,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/modules/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ pub fn installations_subdir(app: AppHandle) -> String {
.unwrap_or_else(|_| "installations".to_string())
}

#[tauri::command]
pub async fn save_file(path: String, contents: String) -> Result<(), String> {
std::fs::write(&path, contents).map_err(|e| e.to_string())
}

pub fn move_folder(source_path: PathBuf, destination_path: PathBuf) -> Result<String, UiError> {
if !source_path.exists() || !source_path.is_dir() {
return Ok("source_not_exist".into());
Expand Down
10 changes: 9 additions & 1 deletion src/components/context-menus/installation.context-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context
import { useNavigate } from "@tanstack/react-router";
import {
DownloadCloudIcon,
DownloadIcon,
FileUpIcon,
FolderOpenIcon,
FolderPenIcon,
Expand All @@ -24,7 +25,7 @@ import { useDownloadVersion } from "@/hooks/use-download-version";
import { useInstalledVersions } from "@/hooks/use-installed-versions";
import { usePlayInstallation } from "@/hooks/use-play-installation";
import { useRevealInFolder } from "@/hooks/use-reveal-in-folder";
import { cn, exportInstallation } from "@/lib/utils";
import { cn, exportInstallation, exportInstallationToFile } from "@/lib/utils";
import { useDialogStore } from "@/stores/dialogs";
import { type Installation, useInstallations } from "@/stores/installations";

Expand Down Expand Up @@ -123,6 +124,13 @@ export const InstallationContextMenu = ({
Export
<FileUpIcon className="inline-block h-4 w-4" />
</ContextMenuItem>
<ContextMenuItem
className="flex items-center justify-between gap-4"
onClick={() => exportInstallationToFile({ installation })}
>
Export to file
<DownloadIcon className="inline-block h-4 w-4" />
</ContextMenuItem>
<ContextMenuItem
className="flex items-center justify-between gap-4"
onClick={() =>
Expand Down
44 changes: 24 additions & 20 deletions src/components/dialogs/addinstallation.dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export function AddInstallationDialog({
}: AddInstallationDialogProps & { open: boolean }) {
const id = useId();
const { data: gameVersions } = useQuery(gameVersionsQuery);
const { installationsParent, installationsSubdir } = useSettingsStore();
const { installationsParent, installationsSubdir, showPreRelease } =
useSettingsStore();
const { appFolder } = useAppFolder();
const { closeDialog } = useDialogStore();
const { addInstallation } = useInstallationsStore();
Expand Down Expand Up @@ -110,7 +111,7 @@ export function AddInstallationDialog({
version ??
gameVersions
?.sort(compareSemverDesc)
.filter((v) => !v.includes("rc"))[0] ??
.filter((v) => showPreRelease || !v.includes("rc"))[0] ??
"",
},
onSubmit: async ({ value }) => {
Expand Down Expand Up @@ -430,24 +431,27 @@ export function AddInstallationDialog({
</p>
</SelectTrigger>
<SelectContent align="start" alignItemWithTrigger={false}>
{gameVersions?.sort(compareSemverDesc).map((version) => (
<SelectItem
className={
installedVersions.includes(version)
? "bg-success/5"
: ""
}
key={version}
value={version}
>
{version}
{installedVersions.includes(version) && (
<span className="text-xs text-muted-foreground opacity-50 ml-2">
(installed)
</span>
)}
</SelectItem>
))}
{gameVersions
?.sort(compareSemverDesc)
.filter((v) => showPreRelease || !v.includes("rc"))
.map((version) => (
<SelectItem
className={
installedVersions.includes(version)
? "bg-success/5"
: ""
}
key={version}
value={version}
>
{version}
{installedVersions.includes(version) && (
<span className="text-xs text-muted-foreground opacity-50 ml-2">
(installed)
</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/components/dialogs/addversion.dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useInstalledVersions } from "@/hooks/use-installed-versions";
import { gameVersionsQuery } from "@/lib/queries";
import { compareSemverDesc } from "@/lib/utils";
import { useDialogStore } from "@/stores/dialogs";
import { useSettingsStore } from "@/stores/settings";

export const versionSchema = z.object({
version: z.string().min(1),
Expand All @@ -37,21 +38,20 @@ export function AddVersionDialog({ open }: { open: boolean }) {
const { data: gameVersions } = useQuery(gameVersionsQuery);
const { closeDialog } = useDialogStore();
const { data: installedVersions } = useInstalledVersions();
const { showPreRelease } = useSettingsStore();
const currentPlatform = platform();

const availableVersions = gameVersions?.filter(
(v) => !installedVersions.includes(v),
(v) =>
!installedVersions.includes(v) && (showPreRelease || !v.includes("rc")),
);

const sortedVersions = gameVersions?.sort(compareSemverDesc);

const { mutateAsync: downloadVersion, isPending } = useDownloadVersion();
const form = useForm({
defaultValues: {
version:
availableVersions
?.sort(compareSemverDesc)
.filter((v) => !v.includes("rc"))[0] ?? "",
version: availableVersions?.sort(compareSemverDesc)[0] ?? "",
},
onSubmit: async ({ value }) => {
await downloadVersion(value.version);
Expand Down
26 changes: 25 additions & 1 deletion src/components/rows/installation.row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useRouter } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
import {
DownloadCloudIcon,
DownloadIcon,
FileUpIcon,
FolderOpenIcon,
PackageOpenIcon,
Expand All @@ -22,7 +23,7 @@ import { useDownloadVersion } from "@/hooks/use-download-version";
import { useInstalledVersions } from "@/hooks/use-installed-versions";
import { usePlayInstallation } from "@/hooks/use-play-installation";
import { useRevealInFolder } from "@/hooks/use-reveal-in-folder";
import { cn, exportInstallation } from "@/lib/utils";
import { cn, exportInstallation, exportInstallationToFile } from "@/lib/utils";
import { useDialogStore } from "@/stores/dialogs";
import { type Installation, useInstallations } from "@/stores/installations";

Expand Down Expand Up @@ -249,6 +250,29 @@ export function InstallationRow({ installation }: InstallationRowProps) {
<TooltipContent>Export</TooltipContent>
</Tooltip>
<GroupSeparator />
<Tooltip>
<TooltipTrigger
render={
<GroupItem
render={
<Button
onClick={() => exportInstallationToFile({ installation })}
size="icon"
variant="outline"
/>
}
>
<DownloadIcon
aria-hidden="true"
className="-ms-1 opacity-60"
size={16}
/>
</GroupItem>
}
/>
<TooltipContent>Export to file</TooltipContent>
</Tooltip>
<GroupSeparator />
<Tooltip>
<TooltipTrigger
render={
Expand Down
37 changes: 37 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { save } from "@tauri-apps/plugin-dialog";
import { platform } from "@tauri-apps/plugin-os";
import { type ClassValue, clsx } from "clsx";
import { toast } from "sonner";
Expand Down Expand Up @@ -157,6 +158,42 @@ export const exportInstallation = async ({
toast.success("Installation copied to clipboard");
};

/**
* Exports the installation data (mods, name, version) to a JSON file via save dialog
* @param installation - The installation to export
*/
export const exportInstallationToFile = async ({
installation,
}: {
installation: Installation;
}) => {
const installationMods = await invoke<{ mods: OutputMod[] }>("get_mods", {
path: installation.path,
});
const data = {
mods: installationMods.mods
.map((m) => ({
id: m.modid,
name: m.name,
version: m.version,
}))
.sort((a, b) => a.id.localeCompare(b.id)),
name: installation.name,
version: installation.version,
};
const filePath = await save({
defaultPath: `${installation.name.replace(/\s+/g, "_")}.json`,
filters: [{ extensions: ["json"], name: "Modpack" }],
});
if (filePath) {
await invoke("save_file", {
contents: JSON.stringify(data, null, 2),
path: filePath,
});
toast.success("Installation exported to file");
}
};

/**
* Variants for the item animations
*/
Expand Down
19 changes: 19 additions & 0 deletions src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const Route = createFileRoute("/settings")({
const settingsSchema = z.object({
darkMode: z.boolean(),
installationsParent: z.string().nullable(),
showPreRelease: z.boolean(),
streamMode: z.boolean(),
versionsParent: z.string().nullable(),
});
Expand Down Expand Up @@ -188,6 +189,7 @@ function RouteComponent() {
defaultValues: {
darkMode: settingsStore.darkMode,
installationsParent: settingsStore.installationsParent,
showPreRelease: settingsStore.showPreRelease,
streamMode: settingsStore.streamMode,
versionsParent: settingsStore.versionsParent,
},
Expand Down Expand Up @@ -224,6 +226,9 @@ function RouteComponent() {
path: value.versionsParent.trim(),
});
}
if (value.showPreRelease !== settingsStore.showPreRelease) {
settingsStore.toggleShowPreRelease();
}
if (value.streamMode !== settingsStore.streamMode) {
settingsStore.toggleStreamMode();
}
Expand Down Expand Up @@ -432,6 +437,20 @@ function RouteComponent() {
</div>
)}
</form.Field>
<form.Field name="showPreRelease">
{(field) => (
<div className="flex items-center gap-3">
<Checkbox
checked={field.state.value}
id="showPreRelease"
onCheckedChange={(checked) => {
field.handleChange(checked === true);
}}
/>
<Label htmlFor="showPreRelease">Show Pre-release Versions</Label>
</div>
)}
</form.Field>
<form.Field name="streamMode">
{(field) => (
<div className="flex items-center gap-3">
Expand Down
5 changes: 5 additions & 0 deletions src/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type SettingsStore = {
) => Promise<void>;
streamMode: boolean;
toggleStreamMode: () => void;
showPreRelease: boolean;
toggleShowPreRelease: () => void;
};

export const useSettingsStore = create<SettingsStore>()((set, _get, store) => ({
Expand Down Expand Up @@ -65,6 +67,7 @@ export const useSettingsStore = create<SettingsStore>()((set, _get, store) => ({
}
set(() => ({ versionsParent: path }));
},
showPreRelease: false,
streamMode: false,
toggleDarkMode: () =>
set((state) => {
Expand All @@ -75,6 +78,8 @@ export const useSettingsStore = create<SettingsStore>()((set, _get, store) => ({
}
return { darkMode: !state.darkMode };
}),
toggleShowPreRelease: () =>
set((state) => ({ showPreRelease: !state.showPreRelease })),
toggleStreamMode: () => set((state) => ({ streamMode: !state.streamMode })),
versionsParent: null,
versionsSubdir: "versions",
Expand Down