mirror of
https://github.com/ow-mods/ow-mod-man.git
synced 2025-12-11 20:15:50 +01:00
Add installing mods
This commit is contained in:
parent
0b6c0231ba
commit
eab1ec9ae5
@ -191,7 +191,7 @@ fn extract_mod_zip(
|
||||
}
|
||||
}
|
||||
|
||||
progress.finish(&format!("Extracted {}", zip_name));
|
||||
progress.finish(&format!("Installed {}", zip_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use owmods_core::db::{fetch_local_db, fetch_remote_db};
|
||||
use owmods_core::download::install_mod_from_db;
|
||||
use owmods_core::mods::{LocalMod, RemoteMod};
|
||||
use owmods_core::open::open_shortcut;
|
||||
use owmods_core::open::{open_readme, open_shortcut};
|
||||
use owmods_core::remove::remove_mod;
|
||||
use tauri::Manager;
|
||||
|
||||
@ -112,6 +113,31 @@ pub async fn toggle_mod(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_mod(
|
||||
unique_name: &str,
|
||||
handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<(), String> {
|
||||
handle.emit_all("INSTALL-START", unique_name).ok();
|
||||
let local_db = state.local_db.read().await;
|
||||
let remote_db = state.remote_db.read().await;
|
||||
let conf = state.config.read().await;
|
||||
let log = get_logger(handle.clone());
|
||||
install_mod_from_db(
|
||||
&log,
|
||||
&unique_name.to_string(),
|
||||
&conf,
|
||||
&remote_db,
|
||||
&local_db,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.map_err(e_to_str)?;
|
||||
handle.emit_all("INSTALL-FINISH", unique_name).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_mod(
|
||||
unique_name: &str,
|
||||
@ -124,3 +150,13 @@ pub async fn uninstall_mod(
|
||||
remove_mod(local_mod, &db, false).map_err(e_to_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_mod_readme(
|
||||
unique_name: &str,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<(), String> {
|
||||
let db = state.remote_db.read().await;
|
||||
open_readme(unique_name, &db).map_err(e_to_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ struct TauriProgressBackend {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProgressStartPayload {
|
||||
id: String,
|
||||
message: String,
|
||||
@ -23,9 +24,13 @@ struct ProgressStartPayload {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum ProgressUpdatePayload {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Increment { id: String, amount: u64 },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChangeMsg { id: String, new_msg: String },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Finish { id: String, msg: String },
|
||||
}
|
||||
|
||||
|
||||
@ -49,7 +49,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
get_remote_mod,
|
||||
open_mod_folder,
|
||||
toggle_mod,
|
||||
uninstall_mod
|
||||
uninstall_mod,
|
||||
install_mod,
|
||||
open_mod_readme
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("Error while running tauri application.");
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"DOWNLOADS": "Downloads",
|
||||
"NO_DOWNLOADS": "No Downloads",
|
||||
"NO_DESCRIPTION": "No Description Provided",
|
||||
"OPEN_WEBSITE": "Show On Website",
|
||||
"OPEN_README": "Open Read Me",
|
||||
"INSTALL": "Install",
|
||||
"INSTALL_FROM": "Install From",
|
||||
"CANCEL": "Cancel",
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
export interface DownloadsBadgeProps {
|
||||
count: number;
|
||||
}
|
||||
import { useTauriCount } from "@hooks";
|
||||
import { memo } from "react";
|
||||
|
||||
const DownloadsBadge = (props: DownloadsBadgeProps) => {
|
||||
return (
|
||||
<div className={`download-badge${props.count === 0 ? " d-none" : ""}`}>{props.count}</div>
|
||||
);
|
||||
};
|
||||
const DownloadsBadge = memo(() => {
|
||||
const count = useTauriCount("INSTALL-START", "INSTALL-FINISH");
|
||||
|
||||
return <div className={`download-badge${count === 0 ? " d-none" : ""}`}>{count}</div>;
|
||||
});
|
||||
|
||||
export default DownloadsBadge;
|
||||
|
||||
@ -1,42 +1,125 @@
|
||||
import { useTranslation } from "@hooks";
|
||||
import { useState } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface ActiveDownloadProps {
|
||||
id: string;
|
||||
progressAction: "Download" | "Extract" | "Wine";
|
||||
progressType: "Definite" | "Indefinite";
|
||||
message: string;
|
||||
progress: number;
|
||||
len: number;
|
||||
}
|
||||
|
||||
interface DownloadPayload {
|
||||
id: string;
|
||||
progress?: number;
|
||||
message: string;
|
||||
interface ProgressIncrementPayload {
|
||||
increment: {
|
||||
id: string;
|
||||
amount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProgressMessagePayload {
|
||||
changeMsg: {
|
||||
id: string;
|
||||
newMsg: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProgressFinishPayload {
|
||||
finish: {
|
||||
id: string;
|
||||
msg: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ActiveDownload = (props: ActiveDownloadProps) => {
|
||||
// Temp state for rn, will use tauri later.
|
||||
const progress = useState<DownloadPayload>({
|
||||
id: props.id,
|
||||
message: "Downloading xen.NewHorizons"
|
||||
})[0];
|
||||
|
||||
return (
|
||||
<div className="downloads-row">
|
||||
<p className="download-header">{progress.message}</p>
|
||||
<progress value={progress.progress} />
|
||||
<p className="download-header">{props.message}</p>
|
||||
<progress
|
||||
value={
|
||||
props.progressType === "Indefinite" && props.progress === 0
|
||||
? undefined
|
||||
: (props.progress / props.len) * 100
|
||||
}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DownloadsPopout = () => {
|
||||
const downloads: ActiveDownloadProps[] = [];
|
||||
const [downloads, setDownloads] = useState<Record<string, ActiveDownloadProps>>({});
|
||||
|
||||
const downloadsRef = useRef(downloads);
|
||||
|
||||
useEffect(() => {
|
||||
downloadsRef.current = downloads;
|
||||
}, [downloads]);
|
||||
|
||||
useEffect(() => {
|
||||
listen("PROGRESS-START", (p) => {
|
||||
const data = p.payload as ActiveDownloadProps;
|
||||
if (data.progressAction !== "Wine") {
|
||||
console.debug(p.payload);
|
||||
if (data.id in downloadsRef.current) {
|
||||
delete downloadsRef.current[data.id];
|
||||
}
|
||||
setDownloads({ ...downloadsRef.current, [data.id]: { ...data, progress: 0 } });
|
||||
}
|
||||
}).catch(console.warn);
|
||||
listen("PROGRESS-INCREMENT", (e) => {
|
||||
const payload = (e.payload as ProgressIncrementPayload).increment;
|
||||
const current = downloadsRef.current[payload.id];
|
||||
if (current) {
|
||||
current.progress += payload.amount;
|
||||
setDownloads({ ...downloadsRef.current, [current.id]: current });
|
||||
}
|
||||
});
|
||||
listen("PROGRESS-MSG", (e) => {
|
||||
const payload = (e.payload as ProgressMessagePayload).changeMsg;
|
||||
const current = downloadsRef.current[payload.id];
|
||||
if (current) {
|
||||
console.debug(payload);
|
||||
current.message = payload.newMsg;
|
||||
setDownloads({ ...downloadsRef.current, [current.id]: current });
|
||||
}
|
||||
});
|
||||
listen("PROGRESS-FINISH", (e) => {
|
||||
const payload = (e.payload as ProgressFinishPayload).finish;
|
||||
const current = downloadsRef.current[payload.id];
|
||||
if (current && current.progressAction === "Extract") {
|
||||
current.message = payload.msg;
|
||||
if (current.progressType === "Indefinite") {
|
||||
current.progress = 1;
|
||||
}
|
||||
setDownloads({ ...downloadsRef.current, [current.id]: current });
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const noDownloads = useTranslation("NO_DOWNLOADS");
|
||||
|
||||
return (
|
||||
<div className="downloads-popout">
|
||||
{downloads.length === 0 ? (
|
||||
{Object.keys(downloads).length === 0 ? (
|
||||
<p className="no-downloads">{noDownloads}</p>
|
||||
) : (
|
||||
downloads.map((d) => <ActiveDownload key={d.id} {...d} />)
|
||||
<>
|
||||
<a
|
||||
role="button"
|
||||
href="#"
|
||||
className="clear-downloads secondary"
|
||||
onClick={() => setDownloads({})}
|
||||
>
|
||||
Clear All
|
||||
</a>
|
||||
{Object.values(downloads)
|
||||
.reverse()
|
||||
.map((d) => (
|
||||
<ActiveDownload key={d.id} {...d} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -4,6 +4,7 @@ export interface ModActionButtonProps {
|
||||
children: ReactNode;
|
||||
ariaLabel: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ModActionButton = (props: ModActionButtonProps) => {
|
||||
@ -12,6 +13,7 @@ const ModActionButton = (props: ModActionButtonProps) => {
|
||||
data-tooltip={props.ariaLabel}
|
||||
data-placement="left" /* Avoid letting the tooltips go out of the window */
|
||||
className="fix-icons"
|
||||
aria-disabled={props.disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onClick?.();
|
||||
|
||||
@ -19,12 +19,15 @@ const LocalModRow = memo((props: LocalModRowProps) => {
|
||||
|
||||
const [showFolderTooltip, uninstallTooltip] = useTranslations(["SHOW_FOLDER", "UNINSTALL"]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
invoke("toggle_mod", {
|
||||
uniqueName: props.uniqueName,
|
||||
enabled: !mod?.enabled ?? false
|
||||
}).then(() => invoke("refresh_local_db"));
|
||||
}, [mod?.enabled]);
|
||||
const onToggle = useCallback(
|
||||
(newVal: boolean) => {
|
||||
invoke("toggle_mod", {
|
||||
uniqueName: props.uniqueName,
|
||||
enabled: newVal
|
||||
}).then(() => invoke("refresh_local_db"));
|
||||
},
|
||||
[props.uniqueName]
|
||||
);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
invoke("open_mod_folder", { uniqueName: props.uniqueName });
|
||||
@ -62,7 +65,7 @@ const LocalModRow = memo((props: LocalModRowProps) => {
|
||||
type="checkbox"
|
||||
aria-label="enabled"
|
||||
role="switch"
|
||||
onClick={onToggle}
|
||||
onChange={(e) => onToggle(e.target.checked)}
|
||||
checked={localMod.enabled}
|
||||
/>
|
||||
</ModHeader>
|
||||
|
||||
@ -2,8 +2,9 @@ import Icon from "@components/Icon";
|
||||
import ModActionButton from "@components/mods/ModActionButton";
|
||||
import ModHeader from "@components/mods/ModHeader";
|
||||
import { useTauri, useTranslations } from "@hooks";
|
||||
import { CSSProperties, memo } from "react";
|
||||
import { FaArrowDown, FaGlobe } from "react-icons/fa";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { CSSProperties, memo, useCallback, useState } from "react";
|
||||
import { FaArrowDown, FaFileAlt } from "react-icons/fa";
|
||||
import { RemoteMod } from "src/types";
|
||||
|
||||
// Stolen from mods website, Rai will never catch me!
|
||||
@ -42,12 +43,28 @@ const RemoteModRow = memo((props: RemoteModRowProps) => {
|
||||
uniqueName: props.uniqueName
|
||||
});
|
||||
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const [noDescription, installTooltip, websiteTooltip] = useTranslations([
|
||||
"NO_DESCRIPTION",
|
||||
"INSTALL",
|
||||
"OPEN_WEBSITE"
|
||||
"OPEN_README"
|
||||
]);
|
||||
|
||||
const onInstall = useCallback(() => {
|
||||
setDownloading(true);
|
||||
invoke("install_mod", { uniqueName: props.uniqueName })
|
||||
.then(() => {
|
||||
setDownloading(false);
|
||||
invoke("refresh_local_db").catch(console.error);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [props.uniqueName]);
|
||||
|
||||
const onReadme = useCallback(() => {
|
||||
invoke("open_mod_readme", { uniqueName: props.uniqueName }).catch(console.warn);
|
||||
}, [props.uniqueName]);
|
||||
|
||||
if (status === "Loading") {
|
||||
return <div className="mod-row center-loading" aria-busy style={props.style}></div>;
|
||||
} else if (status === "Error") {
|
||||
@ -64,11 +81,15 @@ const RemoteModRow = memo((props: RemoteModRowProps) => {
|
||||
<div style={props.style} className="mod-row">
|
||||
<ModHeader {...remote_mod} author={remote_mod.authorDisplay ?? remote_mod.author}>
|
||||
<small>{formatNumber(remote_mod.downloadCount)}</small>
|
||||
<ModActionButton ariaLabel={installTooltip}>
|
||||
<Icon iconType={FaArrowDown} />
|
||||
</ModActionButton>
|
||||
<ModActionButton ariaLabel={websiteTooltip}>
|
||||
<Icon iconType={FaGlobe} />
|
||||
{downloading ? (
|
||||
<div className="center-loading" aria-busy></div>
|
||||
) : (
|
||||
<ModActionButton onClick={onInstall} ariaLabel={installTooltip}>
|
||||
<Icon iconType={FaArrowDown} />
|
||||
</ModActionButton>
|
||||
)}
|
||||
<ModActionButton onClick={onReadme} ariaLabel={websiteTooltip}>
|
||||
<Icon iconType={FaFileAlt} />
|
||||
</ModActionButton>
|
||||
</ModHeader>
|
||||
<small className="mod-description">{desc}</small>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { TranslationContext, TranslationMap } from "@components/TranslationContext";
|
||||
|
||||
export type LoadState = "Loading" | "Done" | "Error";
|
||||
|
||||
const subscribeTauri = (name: string) => {
|
||||
return async (callback: () => void) => {
|
||||
// console.debug("Sub", name);
|
||||
return async (callback: (p: unknown) => void) => {
|
||||
return await listen(name, callback);
|
||||
};
|
||||
};
|
||||
@ -29,7 +30,7 @@ export const useTauri = <T>(
|
||||
|
||||
useEffect(() => {
|
||||
if (status !== "Loading") {
|
||||
console.debug(`Begin subscribe to ${eventName}`);
|
||||
// console.debug(`Begin subscribe to ${eventName}`);
|
||||
subscribeTauri(eventName)(() => setStatus("Loading"))
|
||||
.then((u) => {
|
||||
unsubscribe = u;
|
||||
@ -39,7 +40,7 @@ export const useTauri = <T>(
|
||||
setError(e);
|
||||
});
|
||||
} else {
|
||||
console.debug(`${eventName} Fired, Invoking ${commandName} with args`, commandPayload);
|
||||
// console.debug(`${eventName} Fired, Invoking ${commandName} with args`, commandPayload);
|
||||
getTauriSnapshot(commandName, commandPayload)()
|
||||
.then((data) => {
|
||||
setData(data as T);
|
||||
@ -59,9 +60,29 @@ export const useTauri = <T>(
|
||||
return [status, data, error];
|
||||
};
|
||||
|
||||
export const useTauriCount = (incEvent: string, decEvent: string, initial?: number) => {
|
||||
const [count, setCount] = useState(initial ?? 0);
|
||||
|
||||
const countRef = useRef(initial ?? 0);
|
||||
|
||||
const incCount = () => setCount(countRef.current + 1);
|
||||
const decCount = () => setCount(countRef.current - 1);
|
||||
|
||||
useEffect(() => {
|
||||
countRef.current = count;
|
||||
}, [count]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeTauri(incEvent)(incCount).catch(console.warn);
|
||||
subscribeTauri(decEvent)(decCount).catch(console.warn);
|
||||
}, []);
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
export const useTranslation = (key: string) => {
|
||||
const context = useContext(TranslationContext);
|
||||
console.debug(`Getting Translation For ${key}`);
|
||||
// console.debug(`Getting Translation For ${key}`);
|
||||
return useMemo(() => {
|
||||
const activeTable = TranslationMap[context] ?? TranslationMap["_"];
|
||||
return activeTable[key] ?? activeTable["_"];
|
||||
|
||||
@ -25,6 +25,15 @@
|
||||
background-color: $accent-bg-darker;
|
||||
}
|
||||
|
||||
.clear-downloads {
|
||||
position: absolute;
|
||||
top: $margin-md;
|
||||
right: 0;
|
||||
padding: 0.1em $margin !important;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.downloads-popout p {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@ -15,6 +15,10 @@ $row-border: 1px solid $accent-bg;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mod-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mod-row.local {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
@ -65,6 +69,10 @@ $row-border: 1px solid $accent-bg;
|
||||
column-gap: $margin;
|
||||
}
|
||||
|
||||
.mod-actions.center-loading {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mod-actions svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user