Select file and plugin, add to new plugins state
This commit is contained in:
parent
a067f21f15
commit
2065b5fa3a
@ -1,79 +1,105 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import React from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import useSWRImmutable from "swr/immutable";
|
import useSWRImmutable from "swr/immutable";
|
||||||
|
|
||||||
import { Mod, NEXUS_MODS_URL } from "./ModData";
|
import { Mod, File, NEXUS_MODS_URL } from "./ModData";
|
||||||
import styles from "../styles/AddModData.module.css";
|
import styles from "../styles/AddModData.module.css";
|
||||||
import { jsonFetcher } from "../lib/api";
|
import { jsonFetcher } from "../lib/api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedMod: number;
|
selectedMod: number;
|
||||||
|
selectedPlugin: string | null;
|
||||||
|
setSelectedPlugin: (plugin: string) => void;
|
||||||
counts: Record<number, [number, number, number]> | null;
|
counts: Record<number, [number, number, number]> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddModData: React.FC<Props> = ({ selectedMod, counts }) => {
|
const AddModData: React.FC<Props> = ({
|
||||||
const { data, error } = useSWRImmutable(
|
selectedMod,
|
||||||
`https://mods.modmapper.com/${selectedMod}.json`,
|
selectedPlugin,
|
||||||
|
setSelectedPlugin,
|
||||||
|
counts,
|
||||||
|
}) => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const { data: modData, error: modError } = useSWRImmutable(
|
||||||
|
selectedMod ? `https://mods.modmapper.com/${selectedMod}.json` : null,
|
||||||
(_) => jsonFetcher<Mod>(_)
|
(_) => jsonFetcher<Mod>(_)
|
||||||
);
|
);
|
||||||
|
const { data: fileData, error: fileError } = useSWRImmutable(
|
||||||
|
selectedFile ? `https://files.modmapper.com/${selectedFile}.json` : null,
|
||||||
|
(_) => jsonFetcher<File>(_)
|
||||||
|
);
|
||||||
|
|
||||||
if (error && error.status === 404) {
|
const handleFileChange = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setSelectedFile(event.target.value);
|
||||||
|
},
|
||||||
|
[setSelectedFile]
|
||||||
|
);
|
||||||
|
const handlePluginChange = useCallback(
|
||||||
|
(event) => {
|
||||||
|
setSelectedPlugin(event.target.value);
|
||||||
|
},
|
||||||
|
[setSelectedPlugin]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (modError && modError.status === 404) {
|
||||||
return <div>Mod could not be found.</div>;
|
return <div>Mod could not be found.</div>;
|
||||||
} else if (error) {
|
} else if (modError) {
|
||||||
return <div>{`Error loading mod data: ${error.message}`}</div>;
|
return <div>{`Error loading mod data: ${modError.message}`}</div>;
|
||||||
}
|
}
|
||||||
if (data === undefined)
|
if (modData === undefined)
|
||||||
return <div className={styles.status}>Loading...</div>;
|
return <div className={styles.status}>Loading...</div>;
|
||||||
if (data === null)
|
if (modData === null)
|
||||||
return <div className={styles.status}>Mod could not be found.</div>;
|
return <div className={styles.status}>Mod could not be found.</div>;
|
||||||
|
|
||||||
let numberFmt = new Intl.NumberFormat("en-US");
|
let numberFmt = new Intl.NumberFormat("en-US");
|
||||||
const modCounts = counts && counts[data.nexus_mod_id];
|
const modCounts = counts && counts[modData.nexus_mod_id];
|
||||||
const total_downloads = modCounts ? modCounts[0] : 0;
|
const total_downloads = modCounts ? modCounts[0] : 0;
|
||||||
const unique_downloads = modCounts ? modCounts[1] : 0;
|
const unique_downloads = modCounts ? modCounts[1] : 0;
|
||||||
const views = modCounts ? modCounts[2] : 0;
|
const views = modCounts ? modCounts[2] : 0;
|
||||||
|
|
||||||
if (selectedMod && data) {
|
if (selectedMod && modData) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<h3>
|
<h3>
|
||||||
<a
|
<a
|
||||||
href={`${NEXUS_MODS_URL}/mods/${data.nexus_mod_id}`}
|
href={`${NEXUS_MODS_URL}/mods/${modData.nexus_mod_id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
className={styles.name}
|
className={styles.name}
|
||||||
>
|
>
|
||||||
{data.name}
|
{modData.name}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<strong>Category: </strong>
|
<strong>Category: </strong>
|
||||||
<a
|
<a
|
||||||
href={`${NEXUS_MODS_URL}/mods/categories/${data.category_id}`}
|
href={`${NEXUS_MODS_URL}/mods/categories/${modData.category_id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
{data.category_name}
|
{modData.category_name}
|
||||||
</a>
|
</a>
|
||||||
{data.is_translation && <strong> (translation)</strong>}
|
{modData.is_translation && <strong> (translation)</strong>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Author: </strong>
|
<strong>Author: </strong>
|
||||||
<a
|
<a
|
||||||
href={`${NEXUS_MODS_URL}/users/${data.author_id}`}
|
href={`${NEXUS_MODS_URL}/users/${modData.author_id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
{data.author_name}
|
{modData.author_name}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Uploaded:</strong>{" "}
|
<strong>Uploaded:</strong>{" "}
|
||||||
{format(new Date(data.first_upload_at), "d MMM y")}
|
{format(new Date(modData.first_upload_at), "d MMM y")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Last Update:</strong>{" "}
|
<strong>Last Update:</strong>{" "}
|
||||||
{format(new Date(data.last_update_at), "d MMM y")}
|
{format(new Date(modData.last_update_at), "d MMM y")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Total Downloads:</strong> {numberFmt.format(total_downloads)}
|
<strong>Total Downloads:</strong> {numberFmt.format(total_downloads)}
|
||||||
@ -82,6 +108,44 @@ const AddModData: React.FC<Props> = ({ selectedMod, counts }) => {
|
|||||||
<strong>Unique Downloads:</strong>{" "}
|
<strong>Unique Downloads:</strong>{" "}
|
||||||
{numberFmt.format(unique_downloads)}
|
{numberFmt.format(unique_downloads)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles["select-container"]}>
|
||||||
|
<label htmlFor="mod-file-select" className={styles.label}>
|
||||||
|
Select file:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="file"
|
||||||
|
id="mod-file-select"
|
||||||
|
className={styles.select}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
>
|
||||||
|
<option value="">--Select file--</option>
|
||||||
|
{[...modData.files].reverse().map((file) => (
|
||||||
|
<option key={file.nexus_file_id} value={file.nexus_file_id}>
|
||||||
|
{file.name} (v{file.version}) ({file.category})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{fileData && (
|
||||||
|
<div className={styles["select-container"]}>
|
||||||
|
<label htmlFor="file-plugin-select" className={styles.label}>
|
||||||
|
Select plugin:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
name="plugin"
|
||||||
|
id="file-plugin-select"
|
||||||
|
className={styles.select}
|
||||||
|
onChange={handlePluginChange}
|
||||||
|
>
|
||||||
|
<option value="">--Select plugin--</option>
|
||||||
|
{fileData.plugins.map((plugin) => (
|
||||||
|
<option key={plugin.hash} value={plugin.hash}>
|
||||||
|
{plugin.file_path}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useCallback, useState, useRef } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import useSWRImmutable from "swr/immutable";
|
||||||
|
|
||||||
import AddModData from "./AddModData";
|
import AddModData from "./AddModData";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
|
import { jsonFetcher } from "../lib/api";
|
||||||
|
import { updateFetchedPlugin, PluginsByHashWithMods } from "../slices/plugins";
|
||||||
import styles from "../styles/AddModDialog.module.css";
|
import styles from "../styles/AddModDialog.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -11,16 +15,25 @@ type Props = {
|
|||||||
|
|
||||||
const AddModDialog: React.FC<Props> = ({ counts }) => {
|
const AddModDialog: React.FC<Props> = ({ counts }) => {
|
||||||
const [selectedMod, setSelectedMod] = useState<number | null>(null);
|
const [selectedMod, setSelectedMod] = useState<number | null>(null);
|
||||||
|
const [selectedPlugin, setSelectedPlugin] = useState<string | null>(null);
|
||||||
const [dialogShown, setDialogShown] = useState(false);
|
const [dialogShown, setDialogShown] = useState(false);
|
||||||
const searchInput = useRef<HTMLInputElement | null>(null);
|
const searchInput = useRef<HTMLInputElement | null>(null);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onAddModButtonClick = async () => {
|
const { data, error } = useSWRImmutable(
|
||||||
|
selectedPlugin
|
||||||
|
? `https://plugins.modmapper.com/${selectedPlugin}.json`
|
||||||
|
: null,
|
||||||
|
(_) => jsonFetcher<PluginsByHashWithMods>(_)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onAddModButtonClick = useCallback(async () => {
|
||||||
setSelectedMod(null);
|
setSelectedMod(null);
|
||||||
setDialogShown(true);
|
setDialogShown(true);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (searchInput.current) searchInput.current.focus();
|
if (searchInput.current) searchInput.current.focus();
|
||||||
});
|
});
|
||||||
};
|
}, [setSelectedMod, setDialogShown]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -41,7 +54,12 @@ const AddModDialog: React.FC<Props> = ({ counts }) => {
|
|||||||
inputRef={searchInput}
|
inputRef={searchInput}
|
||||||
/>
|
/>
|
||||||
{selectedMod && (
|
{selectedMod && (
|
||||||
<AddModData selectedMod={selectedMod} counts={counts} />
|
<AddModData
|
||||||
|
selectedMod={selectedMod}
|
||||||
|
selectedPlugin={selectedPlugin}
|
||||||
|
setSelectedPlugin={setSelectedPlugin}
|
||||||
|
counts={counts}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<menu>
|
<menu>
|
||||||
<button
|
<button
|
||||||
@ -55,10 +73,11 @@ const AddModDialog: React.FC<Props> = ({ counts }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(`Adding mod ${selectedMod}`);
|
console.log(`Adding mod ${selectedMod} ${selectedPlugin}`);
|
||||||
|
if (data) dispatch(updateFetchedPlugin(data));
|
||||||
setDialogShown(false);
|
setDialogShown(false);
|
||||||
}}
|
}}
|
||||||
disabled={!selectedMod}
|
disabled={!selectedMod || !selectedPlugin || !data}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
|
@ -4,7 +4,7 @@ import useSWRImmutable from "swr/immutable";
|
|||||||
|
|
||||||
import styles from "../styles/CellData.module.css";
|
import styles from "../styles/CellData.module.css";
|
||||||
import ModList from "./ModList";
|
import ModList from "./ModList";
|
||||||
import PluginList from "./PluginsList";
|
import PluginList from "./ParsedPluginsList";
|
||||||
import { jsonFetcher } from "../lib/api";
|
import { jsonFetcher } from "../lib/api";
|
||||||
|
|
||||||
export interface Mod {
|
export interface Mod {
|
||||||
|
@ -12,7 +12,7 @@ type Props = {};
|
|||||||
const DataDirPicker: React.FC<Props> = () => {
|
const DataDirPicker: React.FC<Props> = () => {
|
||||||
const workerPool = useContext(WorkerPoolContext);
|
const workerPool = useContext(WorkerPoolContext);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const plugins = useAppSelector((state) => state.plugins.plugins);
|
const plugins = useAppSelector((state) => state.plugins.parsedPlugins);
|
||||||
const pluginsPending = useAppSelector((state) => state.plugins.pending);
|
const pluginsPending = useAppSelector((state) => state.plugins.pending);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [uploadNoticeShown, setUploadNoticeShown] = useState(false);
|
const [uploadNoticeShown, setUploadNoticeShown] = useState(false);
|
||||||
|
68
components/FetchedPluginsList.tsx
Normal file
68
components/FetchedPluginsList.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useAppSelector, useAppDispatch } from "../lib/hooks";
|
||||||
|
import {
|
||||||
|
disableAllFetchedPlugins,
|
||||||
|
enableAllFetchedPlugins,
|
||||||
|
toggleFetchedPlugin,
|
||||||
|
} from "../slices/plugins";
|
||||||
|
import styles from "../styles/FetchedPluginList.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedCell?: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
const FetchedPluginsList: React.FC<Props> = ({ selectedCell }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const plugins = useAppSelector((state) =>
|
||||||
|
selectedCell
|
||||||
|
? state.plugins.fetchedPlugins.filter((plugin) =>
|
||||||
|
plugin.cells.some(
|
||||||
|
(cell) => cell.x === selectedCell.x && cell.y === selectedCell.y
|
||||||
|
// TODO: support other worlds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: state.plugins.fetchedPlugins
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{plugins.length > 0 && <h2>Added Plugins ({plugins.length})</h2>}
|
||||||
|
{!selectedCell && plugins.length > 0 && (
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button onClick={() => dispatch(enableAllFetchedPlugins())}>
|
||||||
|
Enable all
|
||||||
|
</button>
|
||||||
|
<button onClick={() => dispatch(disableAllFetchedPlugins())}>
|
||||||
|
Disable all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ol
|
||||||
|
className={`${styles["plugin-list"]} ${
|
||||||
|
plugins.length > 0 ? styles["bottom-spacing"] : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{plugins.map((plugin) => (
|
||||||
|
<li key={plugin.hash} title={plugin.plugins[0].file_name}>
|
||||||
|
<input
|
||||||
|
id={plugin.hash}
|
||||||
|
type="checkbox"
|
||||||
|
checked={plugin.enabled ?? false}
|
||||||
|
value={plugin.enabled ? "on" : "off"}
|
||||||
|
onChange={() => dispatch(toggleFetchedPlugin(plugin.hash))}
|
||||||
|
/>
|
||||||
|
<label htmlFor={plugin.hash} className={styles["plugin-label"]}>
|
||||||
|
<Link href={`/?plugin=${plugin.hash}`}>
|
||||||
|
<a>{plugin.plugins[0].file_name}</a>
|
||||||
|
</Link>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FetchedPluginsList;
|
@ -5,7 +5,7 @@ import mapboxgl from "mapbox-gl";
|
|||||||
import useSWRImmutable from "swr/immutable";
|
import useSWRImmutable from "swr/immutable";
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "../lib/hooks";
|
import { useAppDispatch, useAppSelector } from "../lib/hooks";
|
||||||
import { setFetchedPlugin, PluginFile } from "../slices/plugins";
|
import { setSelectedFetchedPlugin, PluginFile } from "../slices/plugins";
|
||||||
import styles from "../styles/Map.module.css";
|
import styles from "../styles/Map.module.css";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import ToggleLayersControl from "./ToggleLayersControl";
|
import ToggleLayersControl from "./ToggleLayersControl";
|
||||||
@ -55,9 +55,11 @@ const Map: React.FC = () => {
|
|||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const plugins = useAppSelector((state) => state.plugins.plugins);
|
const plugins = useAppSelector((state) => state.plugins.parsedPlugins);
|
||||||
const pluginsPending = useAppSelector((state) => state.plugins.pending);
|
const pluginsPending = useAppSelector((state) => state.plugins.pending);
|
||||||
const fetchedPlugin = useAppSelector((state) => state.plugins.fetchedPlugin);
|
const selectedFetchedPlugin = useAppSelector(
|
||||||
|
(state) => state.plugins.selectedFetchedPlugin
|
||||||
|
);
|
||||||
|
|
||||||
const { data: cellsData, error: cellsError } = useSWRImmutable(
|
const { data: cellsData, error: cellsError } = useSWRImmutable(
|
||||||
"https://cells.modmapper.com/edits.json",
|
"https://cells.modmapper.com/edits.json",
|
||||||
@ -243,7 +245,7 @@ const Map: React.FC = () => {
|
|||||||
|
|
||||||
const clearSelectedCells = useCallback(() => {
|
const clearSelectedCells = useCallback(() => {
|
||||||
setSelectedCells(null);
|
setSelectedCells(null);
|
||||||
dispatch(setFetchedPlugin(undefined));
|
dispatch(setSelectedFetchedPlugin(undefined));
|
||||||
if (map.current) {
|
if (map.current) {
|
||||||
map.current.removeFeatureState({ source: "selected-cells-source" });
|
map.current.removeFeatureState({ source: "selected-cells-source" });
|
||||||
map.current.removeFeatureState({ source: "conflicted-cell-source" });
|
map.current.removeFeatureState({ source: "conflicted-cell-source" });
|
||||||
@ -371,12 +373,12 @@ const Map: React.FC = () => {
|
|||||||
if (
|
if (
|
||||||
router.query.plugin &&
|
router.query.plugin &&
|
||||||
typeof router.query.plugin === "string" &&
|
typeof router.query.plugin === "string" &&
|
||||||
fetchedPlugin &&
|
selectedFetchedPlugin &&
|
||||||
fetchedPlugin.cells
|
selectedFetchedPlugin.cells
|
||||||
) {
|
) {
|
||||||
const cells = [];
|
const cells = [];
|
||||||
const cellSet = new Set<number>();
|
const cellSet = new Set<number>();
|
||||||
for (const cell of fetchedPlugin.cells) {
|
for (const cell of selectedFetchedPlugin.cells) {
|
||||||
if (
|
if (
|
||||||
cell.x !== undefined &&
|
cell.x !== undefined &&
|
||||||
cell.y !== undefined &&
|
cell.y !== undefined &&
|
||||||
@ -388,7 +390,7 @@ const Map: React.FC = () => {
|
|||||||
}
|
}
|
||||||
selectCells(cells);
|
selectCells(cells);
|
||||||
}
|
}
|
||||||
}, [heatmapLoaded, fetchedPlugin, selectCells, router.query.plugin]);
|
}, [heatmapLoaded, selectedFetchedPlugin, selectCells, router.query.plugin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!heatmapLoaded) return; // wait for all map layers to load
|
if (!heatmapLoaded) return; // wait for all map layers to load
|
||||||
|
@ -12,6 +12,43 @@ export interface CellCoord {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModFile {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
category: string;
|
||||||
|
nexus_file_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilePlugin {
|
||||||
|
hash: number;
|
||||||
|
file_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileCell {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
file_name: string;
|
||||||
|
nexus_file_id: number;
|
||||||
|
mod_id: number;
|
||||||
|
category: string;
|
||||||
|
version: string;
|
||||||
|
mod_version: string;
|
||||||
|
size: number;
|
||||||
|
uploaded_at: string;
|
||||||
|
created_at: string;
|
||||||
|
downloaded_at: string;
|
||||||
|
has_plugin: boolean;
|
||||||
|
unable_to_extract_plugins: boolean;
|
||||||
|
cells: FileCell[];
|
||||||
|
plugins: FilePlugin[];
|
||||||
|
plugin_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Mod {
|
export interface Mod {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -30,6 +67,7 @@ export interface Mod {
|
|||||||
first_upload_at: string;
|
first_upload_at: string;
|
||||||
last_updated_files_at: string;
|
last_updated_files_at: string;
|
||||||
cells: CellCoord[];
|
cells: CellCoord[];
|
||||||
|
files: ModFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
|
export const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
|
||||||
|
@ -4,21 +4,21 @@ import React from "react";
|
|||||||
import { useAppSelector, useAppDispatch } from "../lib/hooks";
|
import { useAppSelector, useAppDispatch } from "../lib/hooks";
|
||||||
import { excludedPlugins } from "../lib/plugins";
|
import { excludedPlugins } from "../lib/plugins";
|
||||||
import {
|
import {
|
||||||
enableAllPlugins,
|
enableAllParsedPlugins,
|
||||||
disableAllPlugins,
|
disableAllParsedPlugins,
|
||||||
togglePlugin,
|
toggleParsedPlugin,
|
||||||
} from "../slices/plugins";
|
} from "../slices/plugins";
|
||||||
import styles from "../styles/PluginList.module.css";
|
import styles from "../styles/ParsedPluginList.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedCell?: { x: number; y: number };
|
selectedCell?: { x: number; y: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
const PluginsList: React.FC<Props> = ({ selectedCell }) => {
|
const ParsedPluginsList: React.FC<Props> = ({ selectedCell }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const plugins = useAppSelector((state) =>
|
const plugins = useAppSelector((state) =>
|
||||||
selectedCell
|
selectedCell
|
||||||
? state.plugins.plugins.filter((plugin) =>
|
? state.plugins.parsedPlugins.filter((plugin) =>
|
||||||
plugin.parsed?.cells.some(
|
plugin.parsed?.cells.some(
|
||||||
(cell) =>
|
(cell) =>
|
||||||
cell.x === selectedCell.x &&
|
cell.x === selectedCell.x &&
|
||||||
@ -28,7 +28,7 @@ const PluginsList: React.FC<Props> = ({ selectedCell }) => {
|
|||||||
plugin.parsed?.header.masters[0] === "Skyrim.esm"
|
plugin.parsed?.header.masters[0] === "Skyrim.esm"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: state.plugins.plugins
|
: state.plugins.parsedPlugins
|
||||||
);
|
);
|
||||||
const pluginsPending = useAppSelector((state) => state.plugins.pending);
|
const pluginsPending = useAppSelector((state) => state.plugins.pending);
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ const PluginsList: React.FC<Props> = ({ selectedCell }) => {
|
|||||||
{plugins.length > 0 && <h2>Loaded Plugins ({plugins.length})</h2>}
|
{plugins.length > 0 && <h2>Loaded Plugins ({plugins.length})</h2>}
|
||||||
{!selectedCell && plugins.length > 0 && (
|
{!selectedCell && plugins.length > 0 && (
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<button onClick={() => dispatch(enableAllPlugins())}>
|
<button onClick={() => dispatch(enableAllParsedPlugins())}>
|
||||||
Enable all
|
Enable all
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => dispatch(disableAllPlugins())}>
|
<button onClick={() => dispatch(disableAllParsedPlugins())}>
|
||||||
Disable all
|
Disable all
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -60,7 +60,7 @@ const PluginsList: React.FC<Props> = ({ selectedCell }) => {
|
|||||||
}
|
}
|
||||||
checked={plugin.enabled ?? false}
|
checked={plugin.enabled ?? false}
|
||||||
value={plugin.enabled ? "on" : "off"}
|
value={plugin.enabled ? "on" : "off"}
|
||||||
onChange={() => dispatch(togglePlugin(plugin.filename))}
|
onChange={() => dispatch(toggleParsedPlugin(plugin.filename))}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={plugin.filename} className={styles["plugin-label"]}>
|
<label htmlFor={plugin.filename} className={styles["plugin-label"]}>
|
||||||
{excludedPlugins.includes(plugin.filename) ? (
|
{excludedPlugins.includes(plugin.filename) ? (
|
||||||
@ -87,4 +87,4 @@ const PluginsList: React.FC<Props> = ({ selectedCell }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PluginsList;
|
export default ParsedPluginsList;
|
@ -11,7 +11,8 @@ import ModData from "./ModData";
|
|||||||
import PluginDetail from "./PluginDetail";
|
import PluginDetail from "./PluginDetail";
|
||||||
import DataDirPicker from "./DataDirPicker";
|
import DataDirPicker from "./DataDirPicker";
|
||||||
import PluginTxtEditor from "./PluginTxtEditor";
|
import PluginTxtEditor from "./PluginTxtEditor";
|
||||||
import PluginsList from "./PluginsList";
|
import ParsedPluginsList from "./ParsedPluginsList";
|
||||||
|
import FetchedPluginsList from "./FetchedPluginsList";
|
||||||
import styles from "../styles/Sidebar.module.css";
|
import styles from "../styles/Sidebar.module.css";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -154,7 +155,8 @@ const Sidebar: React.FC<Props> = ({
|
|||||||
</p>
|
</p>
|
||||||
<DataDirPicker />
|
<DataDirPicker />
|
||||||
<PluginTxtEditor />
|
<PluginTxtEditor />
|
||||||
<PluginsList />
|
<ParsedPluginsList />
|
||||||
|
<FetchedPluginsList />
|
||||||
<AddModDialog counts={counts} />
|
<AddModDialog counts={counts} />
|
||||||
{renderLastModified(lastModified)}
|
{renderLastModified(lastModified)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addPluginInOrder,
|
addParsedPluginInOrder,
|
||||||
decrementPending,
|
decrementPending,
|
||||||
PluginFile,
|
PluginFile,
|
||||||
} from "../slices/plugins";
|
} from "../slices/plugins";
|
||||||
@ -48,7 +48,7 @@ export class WorkerPool {
|
|||||||
resolve(worker);
|
resolve(worker);
|
||||||
} else if (typeof data !== "string") {
|
} else if (typeof data !== "string") {
|
||||||
store.dispatch(decrementPending(1));
|
store.dispatch(decrementPending(1));
|
||||||
store.dispatch(addPluginInOrder(data));
|
store.dispatch(addParsedPluginInOrder(data));
|
||||||
// Since web assembly memory cannot be shrunk, replace worker with a fresh one to avoid slow repeated
|
// Since web assembly memory cannot be shrunk, replace worker with a fresh one to avoid slow repeated
|
||||||
// invocations on the same worker instance. Repeated invocations are so slow that the delay in creating a
|
// invocations on the same worker instance. Repeated invocations are so slow that the delay in creating a
|
||||||
// new worker is worth it. In practice, there are usually more workers than tasks, so the delay does not slow
|
// new worker is worth it. In practice, there are usually more workers than tasks, so the delay does not slow
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { WorkerPool } from "./WorkerPool";
|
import { WorkerPool } from "./WorkerPool";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
import { clearPlugins, setPending } from "../slices/plugins";
|
import { clearParsedPlugins, setPending } from "../slices/plugins";
|
||||||
|
|
||||||
export const excludedPlugins = [
|
export const excludedPlugins = [
|
||||||
"Skyrim.esm",
|
"Skyrim.esm",
|
||||||
@ -28,7 +28,7 @@ export const parsePluginFiles = (pluginFiles: File[], workerPool: WorkerPool) =>
|
|||||||
alert("Found no plugins in the folder. Please select the Data folder underneath the Skyrim installation folder.");
|
alert("Found no plugins in the folder. Please select the Data folder underneath the Skyrim installation folder.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
store.dispatch(clearPlugins());
|
store.dispatch(clearParsedPlugins());
|
||||||
store.dispatch(setPending(pluginFiles.length));
|
store.dispatch(setPending(pluginFiles.length));
|
||||||
|
|
||||||
pluginFiles.forEach(async (plugin) => {
|
pluginFiles.forEach(async (plugin) => {
|
||||||
|
@ -27,14 +27,14 @@ export interface World {
|
|||||||
form_id: number;
|
form_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Plugin {
|
export interface ParsedPlugin {
|
||||||
header: Header;
|
header: Header;
|
||||||
cells: Cell[];
|
cells: Cell[];
|
||||||
worlds: World[];
|
worlds: World[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginFile {
|
export interface PluginFile {
|
||||||
parsed?: Plugin;
|
parsed?: ParsedPlugin;
|
||||||
filename: string;
|
filename: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
hash: string;
|
hash: string;
|
||||||
@ -65,7 +65,7 @@ export interface File {
|
|||||||
export interface FetchedPlugin {
|
export interface FetchedPlugin {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
hash: bigint;
|
hash: string;
|
||||||
file_id: number;
|
file_id: number;
|
||||||
mod_id: number;
|
mod_id: number;
|
||||||
version: number;
|
version: number;
|
||||||
@ -79,86 +79,125 @@ export interface FetchedPlugin {
|
|||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FetchedCell {
|
||||||
|
x: 0;
|
||||||
|
y: 0;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginsByHashWithMods {
|
export interface PluginsByHashWithMods {
|
||||||
hash: number;
|
hash: string;
|
||||||
plugins: FetchedPlugin[];
|
plugins: FetchedPlugin[];
|
||||||
files: File[];
|
files: File[];
|
||||||
mods: Mod[];
|
mods: Mod[];
|
||||||
cells: Cell[];
|
cells: FetchedCell[];
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginsState = {
|
export type PluginsState = {
|
||||||
plugins: PluginFile[];
|
parsedPlugins: PluginFile[];
|
||||||
fetchedPlugin?: PluginsByHashWithMods;
|
fetchedPlugins: PluginsByHashWithMods[];
|
||||||
|
selectedFetchedPlugin?: PluginsByHashWithMods;
|
||||||
pending: number;
|
pending: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: PluginsState = { plugins: [], pending: 0 };
|
const initialState: PluginsState = { parsedPlugins: [], fetchedPlugins: [], pending: 0 };
|
||||||
|
|
||||||
export const pluginsSlice = createSlice({
|
export const pluginsSlice = createSlice({
|
||||||
name: "plugins",
|
name: "plugins",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
addPlugin: (state, action: PayloadAction<PluginFile>) => ({
|
addParsedPlugin: (state: PluginsState, action: PayloadAction<PluginFile>) => ({
|
||||||
plugins: [...state.plugins, action.payload],
|
...state,
|
||||||
pending: state.pending,
|
parsedPlugins: [...state.parsedPlugins, action.payload],
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
updatePlugin: (state, action: PayloadAction<PluginFile>) => ({
|
addFetchedPlugin: (state: PluginsState, action: PayloadAction<PluginsByHashWithMods>) => ({
|
||||||
plugins: [...state.plugins.filter(plugin => plugin.hash !== action.payload.hash), action.payload],
|
...state,
|
||||||
pending: state.pending,
|
fetchedPlugins: [...state.fetchedPlugins, action.payload],
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
setPlugins: (state, action: PayloadAction<PluginFile[]>) => ({
|
updateParsedPlugin: (state: PluginsState, action: PayloadAction<PluginFile>) => ({
|
||||||
plugins: action.payload,
|
...state,
|
||||||
pending: state.pending,
|
parsedPlugins: [...state.parsedPlugins.filter(plugin => plugin.hash !== action.payload.hash), action.payload],
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
setPending: (state, action: PayloadAction<number>) => ({
|
updateFetchedPlugin: (state: PluginsState, action: PayloadAction<PluginsByHashWithMods>) => ({
|
||||||
plugins: state.plugins,
|
...state,
|
||||||
|
fetchedPlugins: [...state.fetchedPlugins.filter(plugin => plugin.hash !== action.payload.hash), action.payload],
|
||||||
|
}),
|
||||||
|
setParsedPlugins: (state: PluginsState, action: PayloadAction<PluginFile[]>) => ({
|
||||||
|
...state,
|
||||||
|
parsedPlugins: action.payload,
|
||||||
|
}),
|
||||||
|
setFetchedPlugins: (state: PluginsState, action: PayloadAction<PluginsByHashWithMods[]>) => ({
|
||||||
|
...state,
|
||||||
|
fetchedPlugins: action.payload,
|
||||||
|
}),
|
||||||
|
setPending: (state: PluginsState, action: PayloadAction<number>) => ({
|
||||||
|
...state,
|
||||||
pending: action.payload,
|
pending: action.payload,
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
decrementPending: (state, action: PayloadAction<number>) => ({
|
decrementPending: (state: PluginsState, action: PayloadAction<number>) => ({
|
||||||
plugins: state.plugins,
|
...state,
|
||||||
pending: state.pending - action.payload,
|
pending: state.pending - action.payload,
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
togglePlugin: (state, action: PayloadAction<string>) => ({
|
toggleParsedPlugin: (state: PluginsState, action: PayloadAction<string>) => ({
|
||||||
plugins: state.plugins.map((plugin) => (plugin.filename === action.payload ? { ...plugin, enabled: !plugin.enabled } : plugin)),
|
...state,
|
||||||
pending: state.pending,
|
parsedPlugins: state.parsedPlugins.map((plugin) => (plugin.filename === action.payload ? { ...plugin, enabled: !plugin.enabled } : plugin)),
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
enableAllPlugins: (state) => ({
|
toggleFetchedPlugin: (state: PluginsState, action: PayloadAction<string>) => ({
|
||||||
plugins: state.plugins.map((plugin) => ({ ...plugin, enabled: !plugin.parseError && !excludedPlugins.includes(plugin.filename) && true })),
|
...state,
|
||||||
pending: state.pending,
|
fetchedPlugins: state.fetchedPlugins.map((plugin) => (plugin.hash === action.payload ? { ...plugin, enabled: !plugin.enabled } : plugin)),
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
disableAllPlugins: (state) => ({
|
enableAllParsedPlugins: (state: PluginsState) => ({
|
||||||
plugins: state.plugins.map((plugin) => ({ ...plugin, enabled: false })),
|
...state,
|
||||||
pending: state.pending,
|
parsedPlugins: state.parsedPlugins.map((plugin) => ({ ...plugin, enabled: !plugin.parseError && !excludedPlugins.includes(plugin.filename) && true })),
|
||||||
fetchedPlugin: state.fetchedPlugin,
|
|
||||||
}),
|
}),
|
||||||
setFetchedPlugin: (state, action: PayloadAction<PluginsByHashWithMods | undefined>) => ({
|
enableAllFetchedPlugins: (state: PluginsState) => ({
|
||||||
plugins: state.plugins,
|
...state,
|
||||||
pending: state.pending,
|
fetchedPlugins: state.fetchedPlugins.map((plugin) => ({ ...plugin, enabled: true })),
|
||||||
fetchedPlugin: action.payload,
|
|
||||||
}),
|
}),
|
||||||
clearPlugins: () => ({
|
disableAllParsedPlugins: (state: PluginsState) => ({
|
||||||
plugins: [],
|
...state,
|
||||||
|
parsedPlugins: state.parsedPlugins.map((plugin) => ({ ...plugin, enabled: false })),
|
||||||
|
}),
|
||||||
|
disableAllFetchedPlugins: (state: PluginsState) => ({
|
||||||
|
...state,
|
||||||
|
fetchedPlugins: state.fetchedPlugins.map((plugin) => ({ ...plugin, enabled: false })),
|
||||||
|
}),
|
||||||
|
setSelectedFetchedPlugin: (state: PluginsState, action: PayloadAction<PluginsByHashWithMods | undefined>) => ({
|
||||||
|
...state,
|
||||||
|
selectedFetchedPlugin: action.payload,
|
||||||
|
}),
|
||||||
|
clearParsedPlugins: (state: PluginsState) => ({
|
||||||
|
...state,
|
||||||
|
parsedPlugins: [],
|
||||||
pending: 0,
|
pending: 0,
|
||||||
loadedPluginCells: [],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { addPlugin, updatePlugin, setPlugins, setPending, decrementPending, togglePlugin, enableAllPlugins, disableAllPlugins, setFetchedPlugin, clearPlugins } = pluginsSlice.actions
|
export const {
|
||||||
|
addParsedPlugin,
|
||||||
|
addFetchedPlugin,
|
||||||
|
updateParsedPlugin,
|
||||||
|
updateFetchedPlugin,
|
||||||
|
setParsedPlugins,
|
||||||
|
setFetchedPlugins,
|
||||||
|
setPending,
|
||||||
|
decrementPending,
|
||||||
|
toggleParsedPlugin,
|
||||||
|
toggleFetchedPlugin,
|
||||||
|
enableAllParsedPlugins,
|
||||||
|
enableAllFetchedPlugins,
|
||||||
|
disableAllParsedPlugins,
|
||||||
|
disableAllFetchedPlugins,
|
||||||
|
setSelectedFetchedPlugin,
|
||||||
|
clearParsedPlugins,
|
||||||
|
} = pluginsSlice.actions;
|
||||||
|
|
||||||
export const selectPlugins = (state: AppState) => state.plugins
|
export const selectPlugins = (state: AppState) => state.plugins
|
||||||
|
|
||||||
export const applyLoadOrder = (): AppThunk => (dispatch, getState) => {
|
export const applyLoadOrder = (): AppThunk => (dispatch, getState) => {
|
||||||
const { plugins, pluginsTxt } = getState();
|
const { plugins, pluginsTxt } = getState();
|
||||||
const originalPlugins = [...plugins.plugins];
|
const originalPlugins = [...plugins.parsedPlugins];
|
||||||
let newPlugins = [];
|
let newPlugins = [];
|
||||||
for (let line of pluginsTxt.split("\n")) {
|
for (let line of pluginsTxt.split("\n")) {
|
||||||
let enabled = false;
|
let enabled = false;
|
||||||
@ -179,11 +218,11 @@ export const applyLoadOrder = (): AppThunk => (dispatch, getState) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dispatch(setPlugins([...originalPlugins.sort((a, b) => b.lastModified - a.lastModified), ...newPlugins]));
|
dispatch(setParsedPlugins([...originalPlugins.sort((a, b) => b.lastModified - a.lastModified), ...newPlugins]));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addPluginInOrder = (plugin: PluginFile): AppThunk => (dispatch) => {
|
export const addParsedPluginInOrder = (plugin: PluginFile): AppThunk => (dispatch) => {
|
||||||
dispatch(updatePlugin(plugin));
|
dispatch(updateParsedPlugin(plugin));
|
||||||
dispatch(applyLoadOrder());
|
dispatch(applyLoadOrder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,3 +7,23 @@ a.name {
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
42
styles/FetchedPluginList.module.css
Normal file
42
styles/FetchedPluginList.module.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.plugin-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-list li {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-spacing {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-error {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-label {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-right: 12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user