Add installing mods

This commit is contained in:
Ben C 2023-02-10 12:43:29 -05:00
parent 0b6c0231ba
commit eab1ec9ae5
13 changed files with 237 additions and 48 deletions

View File

@ -191,7 +191,7 @@ fn extract_mod_zip(
}
}
progress.finish(&format!("Extracted {}", zip_name));
progress.finish(&format!("Installed {}", zip_name));
Ok(())
}

View File

@ -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(())
}

View File

@ -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 },
}

View File

@ -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.");

View File

@ -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",

View File

@ -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;

View File

@ -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>
);

View File

@ -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?.();

View File

@ -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>

View File

@ -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>

View File

@ -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["_"];

View File

@ -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;

View File

@ -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;
}