diff --git a/components/Map.tsx b/components/Map.tsx index c5c9693..364fa30 100644 --- a/components/Map.tsx +++ b/components/Map.tsx @@ -4,8 +4,8 @@ import Gradient from "javascript-color-gradient"; import mapboxgl from "mapbox-gl"; import useSWRImmutable from "swr/immutable"; -import { useAppSelector } from "../lib/hooks"; -import { PluginFile } from "../slices/plugins"; +import { useAppDispatch, useAppSelector } from "../lib/hooks"; +import { PluginFile, setFetchedPlugin } from "../slices/plugins"; import styles from "../styles/Map.module.css"; import Sidebar from "./Sidebar"; import ToggleLayersControl from "./ToggleLayersControl"; @@ -59,9 +59,11 @@ const Map: React.FC = () => { | null >(null); const [sidebarOpen, setSidebarOpen] = useState(true); + const dispatch = useAppDispatch(); const plugins = useAppSelector((state) => state.plugins.plugins); const pluginsPending = useAppSelector((state) => state.plugins.pending); + const fetchedPlugin = useAppSelector((state) => state.plugins.fetchedPlugin); const { data: cellsData, error: cellsError } = useSWRImmutable( "https://cells.modmapper.com/edits.json", @@ -246,6 +248,10 @@ const Map: React.FC = () => { const clearSelectedCells = useCallback(() => { setSelectedCells(null); + if (router.query.plugin !== fetchedPlugin?.hash.toString(36)) { + console.log("clearing fetched plugin"); + dispatch(setFetchedPlugin(undefined)); + } if (map.current) { map.current.removeFeatureState({ source: "selected-cell-source" }); map.current.removeFeatureState({ source: "conflicted-cell-source" }); @@ -253,7 +259,7 @@ const Map: React.FC = () => { requestAnimationFrame(() => { if (map.current) map.current.resize(); }); - }, [map]); + }, [map, fetchedPlugin, dispatch, router.query.plugin]); const clearSelectedMod = useCallback(() => { requestAnimationFrame(() => { @@ -305,7 +311,7 @@ const Map: React.FC = () => { if (plugin && plugin.parsed) { const cells = []; const cellSet = new Set(); - for (const cell of plugin.parsed.cells) { + for (const cell of plugin?.parsed?.cells) { if ( cell.x !== undefined && cell.y !== undefined && @@ -361,6 +367,25 @@ const Map: React.FC = () => { pluginsPending, ]); + useEffect(() => { + if (router.query.plugin && typeof router.query.plugin === "string" && fetchedPlugin && fetchedPlugin.cells) { + console.log("selecting fetchedPlugin cells"); + const cells = []; + const cellSet = new Set(); + for (const cell of fetchedPlugin.cells) { + if ( + cell.x !== undefined && + cell.y !== undefined && + cellSet.has(cell.x + cell.y * 1000) === false + ) { + cells.push({ x: cell.x, y: cell.y }); + cellSet.add(cell.x + cell.y * 1000); + } + } + selectCells(cells); + } + }, [fetchedPlugin, selectCells, router.query.plugin]); + useEffect(() => { if (!heatmapLoaded) return; // wait for all map layers to load if (!router.query.mod || typeof router.query.mod !== "string") { diff --git a/components/PluginDetail.tsx b/components/PluginDetail.tsx index f410ca0..3a5d066 100644 --- a/components/PluginDetail.tsx +++ b/components/PluginDetail.tsx @@ -1,60 +1,12 @@ -import React from "react"; +import React, { useEffect } from "react"; import useSWRImmutable from "swr/immutable"; -import { useAppSelector } from "../lib/hooks"; -import { PluginFile } from "../slices/plugins"; -import { Mod } from "./ModData"; -import { Cell } from "./CellData"; +import { useAppDispatch, useAppSelector } from "../lib/hooks"; +import { setFetchedPlugin, PluginFile, PluginsByHashWithMods, Cell } from "../slices/plugins"; import CellModList from "./CellModList"; import PluginData, { Plugin as PluginProps } from "./PluginData"; import styles from "../styles/PluginData.module.css"; -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?: Date; - has_download_link: boolean; - updated_at: Date; - created_at: Date; - downloaded_at?: Date; - has_plugin: boolean; - unable_to_extract_plugins: boolean; -} - -export interface Plugin { - id: number; - name: string; - hash: bigint; - file_id: number; - mod_id: number; - version: number; - size: number; - author?: string; - description?: string; - masters: string[]; - file_name: string; - file_path: string; - updated_at: Date; - created_at: Date; -} - -export interface PluginsByHashWithMods { - hash: number; - plugins: Plugin[]; - files: File[]; - mods: Mod[]; - cells: Cell[]; -} - -const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; - const jsonFetcher = async (url: string): Promise => { const res = await fetch(url); @@ -81,7 +33,6 @@ const buildPluginProps = (data?: PluginsByHashWithMods | null, plugin?: PluginFi } } - type Props = { hash: string; counts: Record | null; @@ -93,9 +44,18 @@ const PluginDetail: React.FC = ({ hash, counts }) => { jsonFetcher ); + const dispatch = useAppDispatch(); const plugins = useAppSelector((state) => state.plugins.plugins); + const fetchedPlugin = useAppSelector((state) => state.plugins.fetchedPlugin); const plugin = plugins.find((plugin) => plugin.hash === hash); + useEffect(() => { + if (data && !fetchedPlugin) { + console.log("setting fetched plugins from PluginDetail", data); + dispatch(setFetchedPlugin(data)); + } + }, [dispatch, data, fetchedPlugin]) + if (!plugin && error && error.status === 404) { return

Plugin could not be found.

; } else if (!plugin && error) { diff --git a/slices/plugins.ts b/slices/plugins.ts index 3f05adb..db16d5b 100644 --- a/slices/plugins.ts +++ b/slices/plugins.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit" import type { AppState, AppThunk } from "../lib/store" +import { Mod } from "../components/ModData"; export interface Header { author?: string; @@ -41,8 +42,53 @@ export interface PluginFile { size: 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?: Date; + has_download_link: boolean; + updated_at: Date; + created_at: Date; + downloaded_at?: Date; + has_plugin: boolean; + unable_to_extract_plugins: boolean; +} + +export interface FetchedPlugin { + id: number; + name: string; + hash: bigint; + file_id: number; + mod_id: number; + version: number; + size: number; + author?: string; + description?: string; + masters: string[]; + file_name: string; + file_path: string; + updated_at: Date; + created_at: Date; +} + +export interface PluginsByHashWithMods { + hash: number; + plugins: FetchedPlugin[]; + files: File[]; + mods: Mod[]; + cells: Cell[]; +} + export type PluginsState = { plugins: PluginFile[]; + fetchedPlugin?: PluginsByHashWithMods; pending: number; } @@ -52,17 +98,50 @@ export const pluginsSlice = createSlice({ name: "plugins", initialState, reducers: { - addPlugin: (state, action: PayloadAction) => ({ plugins: [...state.plugins, action.payload], pending: state.pending }), - updatePlugin: (state, action: PayloadAction) => ({ plugins: [...state.plugins.filter(plugin => plugin.hash !== action.payload.hash), action.payload], pending: state.pending }), - setPlugins: (state, action: PayloadAction) => ({ plugins: action.payload, pending: state.pending }), - setPending: (state, action: PayloadAction) => ({ plugins: state.plugins, pending: action.payload }), - decrementPending: (state, action: PayloadAction) => ({ plugins: state.plugins, pending: state.pending - action.payload }), - togglePlugin: (state, action: PayloadAction) => ({ plugins: state.plugins.map((plugin) => (plugin.filename === action.payload ? { ...plugin, enabled: !plugin.enabled } : plugin)), pending: state.pending }), - clearPlugins: () => ({ plugins: [], pending: 0 }), + addPlugin: (state, action: PayloadAction) => ({ + plugins: [...state.plugins, action.payload], + pending: state.pending, + fetchedPlugin: state.fetchedPlugin, + }), + updatePlugin: (state, action: PayloadAction) => ({ + plugins: [...state.plugins.filter(plugin => plugin.hash !== action.payload.hash), action.payload], + pending: state.pending, + fetchedPlugin: state.fetchedPlugin, + }), + setPlugins: (state, action: PayloadAction) => ({ + plugins: action.payload, + pending: state.pending, + fetchedPlugin: state.fetchedPlugin, + }), + setPending: (state, action: PayloadAction) => ({ + plugins: state.plugins, + pending: action.payload, + fetchedPlugin: state.fetchedPlugin, + }), + decrementPending: (state, action: PayloadAction) => ({ + plugins: state.plugins, + pending: state.pending - action.payload, + fetchedPlugin: state.fetchedPlugin, + }), + togglePlugin: (state, action: PayloadAction) => ({ + plugins: state.plugins.map((plugin) => (plugin.filename === action.payload ? { ...plugin, enabled: !plugin.enabled } : plugin)), + pending: state.pending, + fetchedPlugin: state.fetchedPlugin, + }), + setFetchedPlugin: (state, action: PayloadAction) => ({ + plugins: state.plugins, + pending: state.pending, + fetchedPlugin: action.payload, + }), + clearPlugins: () => ({ + plugins: [], + pending: 0, + loadedPluginCells: [], + }), }, }) -export const { addPlugin, setPlugins, setPending, decrementPending, togglePlugin, clearPlugins } = pluginsSlice.actions +export const { addPlugin, setPlugins, setPending, decrementPending, togglePlugin, setFetchedPlugin, clearPlugins } = pluginsSlice.actions export const selectPlugins = (state: AppState) => state.plugins