Add simple mod loading

This commit is contained in:
Ben C 2023-02-07 11:04:34 -05:00
parent 2b685aca58
commit 6374c5627e
20 changed files with 590 additions and 107 deletions

6
Cargo.lock generated
View File

@ -34,9 +34,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.68"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "async-recursion"
@ -1935,6 +1935,8 @@ dependencies = [
name = "owmods_gui"
version = "0.1.0"
dependencies = [
"anyhow",
"owmods_core",
"serde",
"serde_json",
"tauri",

View File

@ -23,11 +23,18 @@ pub struct RemoteDatabase {
pub mods: HashMap<String, RemoteMod>,
}
#[derive(Clone)]
pub struct LocalDatabase {
pub mods: HashMap<String, LocalMod>,
}
impl RemoteDatabase {
pub fn empty() -> RemoteDatabase {
RemoteDatabase {
mods: HashMap::new(),
}
}
pub async fn fetch(log: &Logger, conf: &Config) -> Result<RemoteDatabase, anyhow::Error> {
log!(log, debug, "Fetching Remote DB At {}", conf.database_url);
let resp = reqwest::get(&conf.database_url).await?;
@ -60,6 +67,12 @@ impl RemoteDatabase {
}
impl LocalDatabase {
pub fn empty() -> LocalDatabase {
LocalDatabase {
mods: HashMap::new(),
}
}
pub fn get_mod(&self, unique_name: &str) -> Option<&LocalMod> {
self.mods.get(unique_name)
}

View File

@ -1,14 +1,19 @@
use serde::Serialize;
#[derive(Clone, Serialize)]
pub enum ProgressType {
Definite,
Indefinite,
}
#[derive(Clone, Serialize)]
pub enum ProgressAction {
Download,
Extract,
Wine,
}
#[derive(Clone, Serialize)]
pub struct LogMessage {
pub message: String,
}
@ -21,6 +26,7 @@ impl LogMessage {
}
}
#[derive(Clone, Serialize)]
pub enum Log {
Debug(LogMessage),
Info(LogMessage),
@ -48,7 +54,7 @@ pub trait ProgressHandler: Send + Sync {
}
pub struct Logger {
backend: Box<dyn LoggerBackend>,
pub backend: Box<dyn LoggerBackend>,
}
macro_rules! log_msg {
@ -90,3 +96,45 @@ impl Logger {
log_msg!(Log::Warning, warning);
log_msg!(Log::Error, error);
}
pub struct BasicConsoleBackend;
struct IgnoreProgressHandler;
impl LoggerBackend for BasicConsoleBackend {
fn handle_log(&self, log: Log) {
match log {
Log::Debug(l) => {
println!("[DEBUG]: {}", l.message);
}
Log::Info(l) => {
println!("[INFO]: {}", l.message);
}
Log::Success(l) => {
println!("[SUCCESS]: {}", l.message);
}
Log::Warning(l) => {
println!("[WARNING]: {}", l.message);
}
Log::Error(l) => {
println!("[ERROR]: {}", l.message);
}
};
}
fn create_progress(
&self,
_id: &str,
_msg: &str,
_progress_type: ProgressType,
_action_type: ProgressAction,
_len: u64,
) -> Box<dyn ProgressHandler> {
Box::new(IgnoreProgressHandler)
}
}
impl ProgressHandler for IgnoreProgressHandler {
fn increment(&self, _amount: u64) {}
fn change_message(&self, _new_message: &str) {}
fn finish(&self, _msg: &str) {}
}

View File

@ -55,7 +55,7 @@ pub struct ModReadMe {
pub download_url: String,
}
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub struct LocalMod {
pub enabled: bool,
pub errors: Vec<String>,

View File

@ -1,6 +1,7 @@
[package]
name = "owmods_gui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -8,10 +9,12 @@ version = "0.1.0"
tauri-build = { version = "1.2.1", features = [] }
[dependencies]
owmods_core = { path = "../../owmods_core" }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2.4", features = [] }
anyhow = "1.0.69"
[features]
default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]

View File

@ -0,0 +1,86 @@
use owmods_core::db::{fetch_local_db, fetch_remote_db};
use owmods_core::mods::{LocalMod, RemoteMod};
use tauri::Manager;
use crate::State;
use crate::logging::get_logger;
#[tauri::command]
pub fn refresh_local_db(
handle: tauri::AppHandle,
state: tauri::State<'_, State>,
) -> Result<(), String> {
let log = get_logger(handle.clone());
let conf = state.config.read().unwrap();
let local_db = fetch_local_db(&log, &*conf).map_err(|err| err.to_string())?;
{
let mut db = state.local_db.write().unwrap();
*db = local_db;
}
handle.emit_all("LOCAL-REFRESH", "").ok();
Ok(())
}
#[tauri::command]
pub fn get_local_mods(state: tauri::State<'_, State>) -> Vec<String> {
state
.local_db
.read()
.unwrap()
.mods
.values()
.map(|m| m.manifest.unique_name.clone())
.collect()
}
#[tauri::command]
pub fn get_local_mod(unique_name: &str, state: tauri::State<'_, State>) -> Option<LocalMod> {
state
.local_db
.read()
.unwrap()
.get_mod(unique_name)
.map(|m| m.clone())
}
#[tauri::command]
pub async fn refresh_remote_db(
handle: tauri::AppHandle,
state: tauri::State<'_, State>,
) -> Result<(), String> {
let log = get_logger(handle.clone());
// Clones to release lock
let conf = state.config.read().unwrap().clone();
let remote_db = fetch_remote_db(&log, &conf)
.await
.map_err(|e| e.to_string())?;
{
let mut db = state.remote_db.write().unwrap();
*db = remote_db;
}
handle.emit_all("REMOTE-REFRESH", "").ok();
Ok(())
}
#[tauri::command]
pub fn get_remote_mods(state: tauri::State<'_, State>) -> Vec<String> {
state
.remote_db
.read()
.unwrap()
.mods
.values()
.map(|m| m.unique_name.clone())
.collect()
}
#[tauri::command]
pub fn get_remote_mod(unique_name: &str, state: tauri::State<'_, State>) -> Option<RemoteMod> {
state
.remote_db
.read()
.unwrap()
.get_mod(unique_name)
.map(|m| m.clone())
}

View File

@ -0,0 +1,111 @@
use owmods_core::logging::{
Log, Logger, LoggerBackend, ProgressAction, ProgressHandler, ProgressType,
};
use serde::Serialize;
use tauri::{AppHandle, Manager};
struct TauriLogBackend {
app: AppHandle,
}
struct TauriProgressBackend {
id: String,
app: AppHandle,
}
#[derive(Clone, Serialize)]
struct ProgressStartPayload {
id: String,
message: String,
progress_type: ProgressType,
progress_action: ProgressAction,
len: u64,
}
#[derive(Clone, Serialize)]
enum ProgressUpdatePayload {
Increment { id: String, amount: u64 },
ChangeMsg { id: String, new_msg: String },
Finish { id: String, msg: String },
}
impl TauriProgressBackend {
pub fn new(id: &str, app: AppHandle) -> TauriProgressBackend {
TauriProgressBackend {
id: id.to_string(),
app: app,
}
}
}
impl LoggerBackend for TauriLogBackend {
fn handle_log(&self, log: Log) {
self.app.emit_all("LOG", log).ok();
}
fn create_progress(
&self,
id: &str,
msg: &str,
progress_type: ProgressType,
action_type: ProgressAction,
len: u64,
) -> Box<dyn ProgressHandler> {
self.app
.emit_all(
"PROGRESS-START",
ProgressStartPayload {
id: id.to_string(),
message: msg.to_string(),
progress_type: progress_type,
progress_action: action_type,
len: len,
},
)
.ok();
Box::new(TauriProgressBackend::new(id, self.app.clone()))
}
}
impl ProgressHandler for TauriProgressBackend {
fn increment(&self, amount: u64) {
self.app
.emit_all(
"PROGRESS-INCREMENT",
ProgressUpdatePayload::Increment {
id: self.id.clone(),
amount: amount,
},
)
.ok();
}
fn change_message(&self, new_message: &str) {
self.app
.emit_all(
"PROGRESS-MSG",
ProgressUpdatePayload::ChangeMsg {
id: self.id.clone(),
new_msg: new_message.to_string(),
},
)
.ok();
}
fn finish(&self, msg: &str) {
self.app
.emit_all(
"PROGRESS-FINISH",
ProgressUpdatePayload::Finish {
id: self.id.clone(),
msg: msg.to_string(),
},
)
.ok();
}
}
pub fn get_logger(handle: AppHandle) -> Logger {
let backend = TauriLogBackend { app: handle };
Logger::new(Box::new(backend))
}

View File

@ -3,8 +3,54 @@
windows_subsystem = "windows"
)]
fn main() {
use std::{
error::Error,
sync::{Arc, RwLock},
};
use commands::*;
use logging::get_logger;
use owmods_core::{
config::{get_config, Config},
db::{LocalDatabase, RemoteDatabase},
logging::{BasicConsoleBackend, Logger},
};
mod commands;
mod logging;
pub struct State {
local_db: Arc<RwLock<LocalDatabase>>,
remote_db: Arc<RwLock<RemoteDatabase>>,
config: Arc<RwLock<Config>>,
}
fn main() -> Result<(), Box<dyn Error>> {
let basic_console = BasicConsoleBackend;
let temp_logger = Logger::new(Box::new(basic_console));
let config = get_config(&temp_logger)?;
tauri::Builder::default()
.manage(State {
local_db: Arc::new(RwLock::new(LocalDatabase::empty())),
remote_db: Arc::new(RwLock::new(RemoteDatabase::empty())),
config: Arc::new(RwLock::new(config)),
})
.setup(move |app| {
get_logger(app.handle()).debug("Starting App");
Ok(())
})
.invoke_handler(tauri::generate_handler![
refresh_local_db,
get_local_mods,
get_local_mod,
refresh_remote_db,
get_remote_mods,
get_remote_mod
])
.run(tauri::generate_context!())
.expect("Error while running tauri application.");
Ok(())
}

View File

@ -13,14 +13,19 @@
},
"dependencies": {
"@picocss/pico": "^1.5.7",
"@tauri-apps/api": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.7.1"
"react-icons": "^4.7.1",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.8"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"@vitejs/plugin-react": "^3.0.1",

View File

@ -2,9 +2,12 @@ lockfileVersion: 5.4
specifiers:
"@picocss/pico": ^1.5.7
"@tauri-apps/api": ^1.2.0
"@types/node": ^18.11.18
"@types/react": ^18.0.27
"@types/react-dom": ^18.0.10
"@types/react-virtualized-auto-sizer": ^1.0.1
"@types/react-window": ^1.8.5
"@typescript-eslint/eslint-plugin": ^5.50.0
"@typescript-eslint/parser": ^5.50.0
"@vitejs/plugin-react": ^3.0.1
@ -14,20 +17,27 @@ specifiers:
react: ^18.2.0
react-dom: ^18.2.0
react-icons: ^4.7.1
react-virtualized-auto-sizer: ^1.0.7
react-window: ^1.8.8
sass: ^1.58.0
typescript: ^4.9.5
vite: ^4.0.4
dependencies:
"@picocss/pico": 1.5.7
"@tauri-apps/api": 1.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-icons: 4.7.1_react@18.2.0
react-virtualized-auto-sizer: 1.0.7_biqbaboplfbrettd7655fr4n2y
react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y
devDependencies:
"@types/node": 18.11.19
"@types/react": 18.0.27
"@types/react-dom": 18.0.10
"@types/react-virtualized-auto-sizer": 1.0.1
"@types/react-window": 1.8.5
"@typescript-eslint/eslint-plugin": 5.51.0_b635kmla6dsb4frxfihkw4m47e
"@typescript-eslint/parser": 5.51.0_4vsywjlpuriuw3tl5oq6zy5a64
"@vitejs/plugin-react": 3.1.0_vite@4.1.1
@ -296,6 +306,16 @@ packages:
"@babel/helper-plugin-utils": 7.20.2
dev: true
/@babel/runtime/7.20.13:
resolution:
{
integrity: sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
}
engines: { node: ">=6.9.0" }
dependencies:
regenerator-runtime: 0.13.11
dev: false
/@babel/template/7.20.7:
resolution:
{
@ -747,6 +767,14 @@ packages:
}
dev: false
/@tauri-apps/api/1.2.0:
resolution:
{
integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==
}
engines: { node: ">= 14.6.0", npm: ">= 6.6.0", yarn: ">= 1.19.1" }
dev: false
/@types/json-schema/7.0.11:
resolution:
{
@ -777,6 +805,24 @@ packages:
"@types/react": 18.0.27
dev: true
/@types/react-virtualized-auto-sizer/1.0.1:
resolution:
{
integrity: sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==
}
dependencies:
"@types/react": 18.0.27
dev: true
/@types/react-window/1.8.5:
resolution:
{
integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==
}
dependencies:
"@types/react": 18.0.27
dev: true
/@types/react/18.0.27:
resolution:
{
@ -2400,6 +2446,13 @@ packages:
"@jridgewell/sourcemap-codec": 1.4.14
dev: true
/memoize-one/5.2.1:
resolution:
{
integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
}
dev: false
/merge2/1.4.1:
resolution:
{
@ -2757,6 +2810,36 @@ packages:
engines: { node: ">=0.10.0" }
dev: true
/react-virtualized-auto-sizer/1.0.7_biqbaboplfbrettd7655fr4n2y:
resolution:
{
integrity: sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==
}
engines: { node: ">8.0.0" }
peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc
dependencies:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react-window/1.8.8_biqbaboplfbrettd7655fr4n2y:
resolution:
{
integrity: sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==
}
engines: { node: ">8.0.0" }
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
"@babel/runtime": 7.20.13
memoize-one: 5.2.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0:
resolution:
{
@ -2777,6 +2860,13 @@ packages:
picomatch: 2.3.1
dev: true
/regenerator-runtime/0.13.11:
resolution:
{
integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
}
dev: false
/regexp.prototype.flags/1.4.3:
resolution:
{

View File

@ -1,5 +1,9 @@
import Nav from "@components/nav/Nav";
import Tabs from "@components/tabs/Tabs";
import { invoke } from "@tauri-apps/api";
// Refresh once to get data
invoke("refresh_local_db");
const App = () => {
return (

View File

@ -4,18 +4,20 @@ import DownloadsBadge from "./DownloadsBadge";
import DownloadsPopout from "./DownloadsPopout";
import NavButton from "../nav/NavButton";
const Downloads = () => {
return <li>
<details role="list">
<summary>
<NavButton labelPlacement="right" ariaLabel="Downloads">
<Icon iconType={FaArrowDown} />
<DownloadsBadge count={3} />
</NavButton>
</summary>
<DownloadsPopout/>
</details>
</li> ;
}
const Downloads = () => {
return (
<li>
<details role="list">
<summary>
<NavButton labelPlacement="right" ariaLabel="Downloads">
<Icon iconType={FaArrowDown} />
<DownloadsBadge count={3} />
</NavButton>
</summary>
<DownloadsPopout />
</details>
</li>
);
};
export default Downloads;

View File

@ -5,15 +5,17 @@ interface ActiveDownloadProps {
}
interface DownloadPayload {
id: string,
progress?: number,
message: string
id: string;
progress?: number;
message: string;
}
const ActiveDownload = (props: ActiveDownloadProps) => {
// Temp state for rn, will use useSyncExternalStore later.
const [progress, setProgress] = useState<DownloadPayload>({id: props.id, message: "Downloading xen.NewHorizons"});
const [progress, setProgress] = useState<DownloadPayload>({
id: props.id,
message: "Downloading xen.NewHorizons"
});
return (
<div className="downloads-row">

View File

@ -1,34 +1,45 @@
import Icon from "@components/Icon";
import ModActionButton from "@components/mods/ModActionButton";
import ModHeader from "@components/mods/ModHeader";
import { useTauri } from "@hooks";
import { LocalMod } from "@types";
import { memo } from "react";
import { FaFolder, FaTrash } from "react-icons/fa";
import { LocalMod } from "src/types";
const isEqual = (prev: LocalMod, next: LocalMod) =>
prev.manifest.uniqueName === next.manifest.uniqueName;
const LocalModRow = memo((props: LocalMod) => {
return (
<details>
<ModHeader {...props.manifest}>
<ModActionButton ariaLabel="Show Folder">
<Icon iconType={FaFolder} />
</ModActionButton>
<ModActionButton ariaLabel="Uninstall Mod">
<Icon iconType={FaTrash} />
</ModActionButton>
<input
className="mod-toggle"
type="checkbox"
aria-label="enabled"
role="switch"
value={props.enabled.toString()}
/>
</ModHeader>
Description Not Available
</details>
const LocalModRow = memo((props: { uniqueName: string }) => {
const [status, mod, err] = useTauri<LocalMod, { uniqueName: string }>(
"LOCAL-REFRESH",
"get_local_mod",
{ uniqueName: props.uniqueName }
);
}, isEqual);
if (status === "Loading") {
return <p>Loading</p>;
} else if (status === "Error") {
return <p>{err!}</p>;
} else {
const localMod = mod!;
return (
<details>
<ModHeader {...localMod.manifest}>
<ModActionButton ariaLabel="Show Folder">
<Icon iconType={FaFolder} />
</ModActionButton>
<ModActionButton ariaLabel="Uninstall Mod">
<Icon iconType={FaTrash} />
</ModActionButton>
<input
className="mod-toggle"
type="checkbox"
aria-label="enabled"
role="switch"
checked={localMod.enabled}
/>
</ModHeader>
Description Not Available
</details>
);
}
});
export default LocalModRow;

View File

@ -1,27 +1,23 @@
import LocalModRow from "@components/mods/local/LocalModRow";
import { LocalMod } from "src/types";
import { useTauri } from "@hooks";
import LocalModRow from "./LocalModRow";
const LocalMods = () => {
const mods: LocalMod[] = [
{
enabled: true,
modPath: "C:/",
manifest: {
uniqueName: "Bwc9876.TimeSaver",
author: "Bwc9876",
name: "Time Saver",
version: "0.0.1"
}
}
];
const [status, mods, err] = useTauri<string[], undefined>("LOCAL-REFRESH", "get_local_mods");
return (
<div className="mod-list">
{mods.map((c) => (
<LocalModRow key={c.manifest.uniqueName} {...c} />
))}
</div>
);
switch (status) {
case "Loading":
return <p>Loading</p>;
case "Done":
return (
<div className="mod-list">
{mods!.map((m) => (
<LocalModRow key={m} uniqueName={m} />
))}
</div>
);
case "Error":
return <div>{err!}</div>;
}
};
export default LocalMods;

View File

@ -1,25 +1,39 @@
import Icon from "@components/Icon";
import ModActionButton from "@components/mods/ModActionButton";
import ModHeader from "@components/mods/ModHeader";
import { useTauri } from "@hooks";
import { memo } from "react";
import { FaArrowDown, FaGlobe } from "react-icons/fa";
import { RemoteMod } from "src/types";
const RemoteModRow = memo((props: RemoteMod) => {
return (
<details>
<ModHeader {...props}>
<small>{props.downloadCount}</small>
<ModActionButton ariaLabel="Install With Dependencies">
<Icon iconType={FaArrowDown} />
</ModActionButton>
<ModActionButton ariaLabel="View On Website">
<Icon iconType={FaGlobe} />
</ModActionButton>
</ModHeader>
<small>{props.description}</small>
</details>
const RemoteModRow = memo((props: { uniqueName: string }) => {
const [status, mod, err] = useTauri<RemoteMod, { uniqueName: string }>(
"REMOTE-REFRESH",
"get_load_mod",
{ uniqueName: props.uniqueName }
);
if (status === "Loading") {
return <p>Loading...</p>;
} else if (status === "Error") {
return <p>{err}</p>;
} else {
const remote_mod = mod!;
return (
<details>
<ModHeader {...remote_mod}>
<small>{remote_mod.downloadCount}</small>
<ModActionButton ariaLabel="Install With Dependencies">
<Icon iconType={FaArrowDown} />
</ModActionButton>
<ModActionButton ariaLabel="View On Website">
<Icon iconType={FaGlobe} />
</ModActionButton>
</ModHeader>
<small>{remote_mod.description}</small>
</details>
);
}
});
export default RemoteModRow;

View File

@ -1,27 +1,24 @@
import { useTauri } from "@hooks";
import { RemoteMod } from "src/types";
import RemoteModRow from "./RemoteModRow";
const RemoteMods = () => {
const mods: RemoteMod[] = [
{
uniqueName: "Bwc9876.TimeSaver",
downloadUrl: "google",
downloadCount: 50,
description: "abooga",
repo: "ff",
author: "Bwc9876",
name: "Time Saver",
version: "0.0.1"
}
];
const [status, mods, err] = useTauri<string[], undefined>("REMOTE-REFRESH", "get_remote_mods");
return (
<div className="mod-list">
{mods.map((c) => (
<RemoteModRow {...c} key={c.uniqueName} />
))}
</div>
);
if (status === "Loading") {
return <p>Loading...</p>;
} else if (status === "Error") {
return <p>{err}</p>;
} else {
const remote_mods = mods!;
return (
<div className="mod-list">
{remote_mods.map((m) => (
<RemoteModRow key={m} uniqueName={m} />
))}
</div>
);
}
};
export default RemoteMods;

View File

@ -0,0 +1,51 @@
import { invoke } from "@tauri-apps/api";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";
export type LoadState = "Loading" | "Done" | "Error";
const subscribeTauri = (name: string) => {
return async (callback: () => void) => {
return await listen(name, callback);
};
};
const getTauriSnapshot = <T, P>(cmdName: string, payload: P): (() => Promise<T>) => {
return async () => {
return await invoke(cmdName, payload as Record<string, unknown>);
};
};
export const useTauri = <T, P>(
eventName: string,
commandName: string,
commandPayload?: P
): [LoadState, T | null, string | null] => {
const [status, setStatus] = useState<LoadState>("Loading");
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let u = () => {};
if (status !== "Loading") {
console.debug(`Begin subscribe to ${eventName}`);
subscribeTauri(eventName)(() => setStatus("Loading")).then((unsubscribe) => {
u = unsubscribe;
});
} else {
console.debug(`Invoking ${commandName} with args ${commandPayload ?? "null"}`);
getTauriSnapshot(commandName, commandPayload)()
.then((data) => {
setData(data as T);
setStatus("Done");
})
.catch((e) => {
setError(e as string);
setStatus("Error");
});
}
return u;
}, [status]);
return [status, data, error];
};

View File

@ -20,7 +20,8 @@
"@components/*": ["./src/components/*"],
"@styles/*": ["./src/styles/*"],
"@assets/*": ["./src/assets/*"],
"@types": ["src/types"]
"@types": ["src/types"],
"@hooks": ["src/hooks"]
}
},
"include": ["src"],

View File

@ -21,7 +21,8 @@ export default defineConfig({
{ find: "@components", replacement: path.resolve(__dirname, "src/components") },
{ find: "@styles", replacement: path.resolve(__dirname, "src/styles") },
{ find: "@assets", replacement: path.resolve(__dirname, "src/assets") },
{ find: "@types", replacement: path.resolve(__dirname, "src/types.ts") }
{ find: "@types", replacement: path.resolve(__dirname, "src/types.ts") },
{ find: "@hooks", replacement: path.resolve(__dirname, "src/hooks.ts") }
]
}
});