diff --git a/.gitignore b/.gitignore index 5aa9223..ed02adb 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,5 @@ temp/ .yarn/cache/ dumps + +sublog.txt diff --git a/locales/en/translation.json b/locales/en/translation.json index 83c0bbe..a103dbb 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -381,5 +381,14 @@ "setFolderPathsManuallyPharaoh": "The mod manager tried to get Pharaoh folder locations from Windows Registry, but it couldn't find them! You'll have to set them manually!", "setFolderPathsManuallyOptionallyPharaoh": "The mod manager automatically found Pharaoh folder paths from the Windows Registry, but you can set them manually here.", "noMissingFilesFound": "No missing files found!", - "missingFiles": "Missing Files" + "missingFiles": "Missing Files", + "dynasties": "Pharaoh Dynasties", + "mainDynastiesFolder": "The main Dynasties folder that contains Pharaoh.exe, for example C:\\Program Files (x86)\\Steam\\steamapps\\common\\Total War PHARAOH DYNASTIES", + "dynastiesFolder": "Dynasties folder:", + "dynastiesContentFolder": "The Dynasties Steam Workshop content folder named 2951630 (which is the steam ID for Pharaoh Dynasties) that contains mods, for example C:\\Program Files (x86)\\Steam\\steamapps\\workshop\\content\\2951630", + "selectDynastiesFolder": "Select Dynasties Folder", + "setFolderPathsManuallyDynasties": "The mod manager tried to get Pharaoh Dynasties folder locations from Windows Registry, but it couldn't find them! You'll have to set them manually!", + "setFolderPathsManuallyOptionallyDynasties": "The mod manager automatically found Pharaoh Dynasties folder paths from the Windows Registry, but you can set them manually here.", + "chooseNewModTag": "Choose tag for the new mod:", + "upload": "Upload" } diff --git a/package.json b/package.json index b25f77c..72cbf3d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wh3mm", "productName": "wh3mm", - "version": "2.7.0", + "version": "2.8.0", "description": "WH3 Mod Manager", "main": ".webpack/main", "scripts": { diff --git a/src/appConfigFunctions.ts b/src/appConfigFunctions.ts index 0ac10e3..0a74996 100644 --- a/src/appConfigFunctions.ts +++ b/src/appConfigFunctions.ts @@ -61,6 +61,7 @@ const removeModDataWeDontSave = (mods: Mod[] | undefined) => { mod.isDeleted = false; mod.isMovie = false; mod.dependencyPacks = []; + mod.tags = []; } }; diff --git a/src/appData.ts b/src/appData.ts index a2cca83..5eb3348 100644 --- a/src/appData.ts +++ b/src/appData.ts @@ -98,6 +98,7 @@ for (const supportedGame of supportedGames) { attila: undefined, troy: undefined, pharaoh: undefined, + dynasties: undefined, }; (appData as AppData).gameToPresets = { wh2: [], @@ -106,6 +107,7 @@ for (const supportedGame of supportedGames) { attila: [], troy: [], pharaoh: [], + dynasties: [], }; export default appData as AppData; diff --git a/src/appSlice.ts b/src/appSlice.ts index 5bde1f9..3c22924 100644 --- a/src/appSlice.ts +++ b/src/appSlice.ts @@ -485,6 +485,7 @@ const appSlice = createSlice({ if (removedModData) { mod.isEnabled = removedModData.isEnabled; mod.loadOrder = removedModData.loadOrder; + mod.tags = removedModData.tags; // state.currentPreset.mods.splice(state.currentPreset.mods.indexOf(mod), 1); // state.currentPreset.mods.splice(removedModData.indexInMods, 0, mod); state.removedModsData = state.removedModsData.filter(({ modPath }) => modPath != mod.path); @@ -560,6 +561,7 @@ const appSlice = createSlice({ indexInMods: state.currentPreset.mods.indexOf(removedMod), loadOrder: removedMod.loadOrder, time: Date.now(), + tags: removedMod.tags, }); state.removedModsCategories[removedMod.path] = removedMod.categories ?? []; @@ -587,6 +589,16 @@ const appSlice = createSlice({ !equal(dataMod.reqModIdToName, data.reqModIdToName) ) dataMod.reqModIdToName = data.reqModIdToName; + if (data.tags) { + if (dataMod.tags.length != data.tags.length) { + // console.log("tags changed:", dataMod.name, dataMod.tags, "->", data.tags); + dataMod.tags = data.tags; + } + if (contentMod.tags.length != data.tags.length) { + // console.log("tags changed:", contentMod.name, contentMod.tags, "->", data.tags); + contentMod.tags = data.tags; + } + } } } @@ -607,6 +619,16 @@ const appSlice = createSlice({ } if (data.lastChanged && mod.lastChanged != data.lastChanged) mod.lastChanged = data.lastChanged; + if (data.tags && mod.tags.length != data.tags.length) { + // console.log("tags changed:", mod.name, mod.tags, "->", data.tags); + mod.tags = data.tags; + } + + // timeAddedToUserList we get from Steam is always 0, not sure what it's for but it's not actually time of subbing + // if (data.subscriptionTime && mod.subbedTime != data.subscriptionTime) { + // console.log("subbedTime:", mod.subbedTime, "->", data.subscriptionTime); + // mod.subbedTime = data.subscriptionTime; + // } } }, setPackHeaderData: (state: AppState, action: PayloadAction) => { @@ -1138,6 +1160,20 @@ const appSlice = createSlice({ createBisectedModListPresets: (state: AppState, action: PayloadAction) => { createBisectedModListPresetsInternal(state, action.payload); }, + setIsModTagPickerOpen: (state: AppState, action: PayloadAction) => { + state.isModTagPickerOpen = action.payload; + }, + setCurrentModToUpload: (state: AppState, action: PayloadAction) => { + state.currentModToUpload = action.payload; + }, + setTagForMod: (state: AppState, action: PayloadAction<{ mod: Mod; tag: string }>) => { + const payloadMod = action.payload.mod; + const payloadTag = action.payload.tag; + const mod = state.currentPreset.mods.find((mod) => mod.path == payloadMod.path); + if (!mod) return; + + mod.tags = ["mod", payloadTag]; + }, selectCategory: (state: AppState, action: PayloadAction) => { const { mods, category, selectOperation } = action.payload; @@ -1243,6 +1279,9 @@ export const { setCustomizableMods, createBisectedModListPresets, requestGameFolderPaths, + setCurrentModToUpload, + setIsModTagPickerOpen, + setTagForMod, } = appSlice.actions; export default appSlice.reducer; diff --git a/src/assets/game_icons/dynasties.png b/src/assets/game_icons/dynasties.png new file mode 100644 index 0000000..118607d Binary files /dev/null and b/src/assets/game_icons/dynasties.png differ diff --git a/src/components/GamePathsSetup.tsx b/src/components/GamePathsSetup.tsx index 12cf3a8..1c77743 100644 --- a/src/components/GamePathsSetup.tsx +++ b/src/components/GamePathsSetup.tsx @@ -24,6 +24,7 @@ const supportedGameToMainGameFolderLocalization: Record attila: "mainAttilaFolder", troy: "mainTroyFolder", pharaoh: "mainPharaohFolder", + dynasties: "mainDynastiesFolder", }; const supportedGameToGameFolderLocalization: Record = { wh2: "wh2Folder", @@ -32,6 +33,7 @@ const supportedGameToGameFolderLocalization: Record = { attila: "attilaFolder", troy: "troyFolder", pharaoh: "pharaohFolder", + dynasties: "dynastiesFolder", }; const supportedGameToContentFolderLocalization: Record = { wh2: "wh2ContentFolder", @@ -40,6 +42,7 @@ const supportedGameToContentFolderLocalization: Record = attila: "attilaContentFolder", troy: "troyContentFolder", pharaoh: "pharaohContentFolder", + dynasties: "dynastiesContentFolder", }; const supportedGameToSelectFolderLocalization: Record = { wh2: "selectWH2Folder", @@ -48,6 +51,7 @@ const supportedGameToSelectFolderLocalization: Record = attila: "selectAttilaFolder", troy: "selectTroyFolder", pharaoh: "selectPharaohFolder", + dynasties: "selectDynastiesFolder", }; const supportedGameToSetFolderPathsManuallyLocalization: Record = { wh2: "setFolderPathsManuallyWH2", @@ -56,6 +60,7 @@ const supportedGameToSetFolderPathsManuallyLocalization: Record = { wh2: "setFolderPathsManuallyOptionallyWH2", @@ -64,6 +69,7 @@ const supportedGameToSetFolderPathsManuallyOptionallyLocalization: Record { @@ -78,6 +84,8 @@ const GamePathsSetup = memo(({ isOpen, setIsOpen }: GamePathsSetupProps) => { const localized: Record = useContext(localizationContext); + console.log("requestFolderPathsForGame:", requestFolderPathsForGame); + return ( <> {(isOpen || (isSetAppFolderPathsDone && isAnyPathEmpty) || requestFolderPathsForGame) && ( @@ -95,7 +103,7 @@ const GamePathsSetup = memo(({ isOpen, setIsOpen }: GamePathsSetupProps) => {
- {(isAnyPathEmpty && ( + {((isAnyPathEmpty || requestFolderPathsForGame) && (

{localized[supportedGameToSetFolderPathsManuallyLocalization[currentGame]]}

diff --git a/src/components/Main.tsx b/src/components/Main.tsx index f526406..24a22ac 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -3,6 +3,7 @@ import { useAppSelector } from "../hooks"; import Sidebar from "./Sidebar"; import ModRows from "./ModRows"; import Categories from "./Categories"; +import ModTagPicker from "./ModTagPicker"; const Main = () => { const currentTab = useAppSelector((state) => state.app.currentTab); @@ -16,6 +17,7 @@ const Main = () => {
+
)} diff --git a/src/components/ModDropdownOptions.tsx b/src/components/ModDropdownOptions.tsx index 1dda45e..fe40e31 100644 --- a/src/components/ModDropdownOptions.tsx +++ b/src/components/ModDropdownOptions.tsx @@ -1,6 +1,12 @@ import { Tooltip } from "flowbite-react"; import React, { memo, useCallback, useContext, useState } from "react"; -import { setModLoadOrderRelativeTo, toggleAlwaysEnabledMods, toggleAlwaysHiddenMods } from "../appSlice"; +import { + setCurrentModToUpload, + setIsModTagPickerOpen, + setModLoadOrderRelativeTo, + toggleAlwaysEnabledMods, + toggleAlwaysHiddenMods, +} from "../appSlice"; import { FaFolderOpen, FaExternalLinkAlt, @@ -100,7 +106,8 @@ const ModDropdownOptions = memo((props: ModDropdownOptionsProps) => { ); const uploadMod = useCallback( (mod: Mod) => { - window.api?.uploadMod(mod); + dispatch(setCurrentModToUpload(mod)); + dispatch(setIsModTagPickerOpen(true)); }, [allMods] ); diff --git a/src/components/ModTagPicker.tsx b/src/components/ModTagPicker.tsx new file mode 100644 index 0000000..c055606 --- /dev/null +++ b/src/components/ModTagPicker.tsx @@ -0,0 +1,94 @@ +import { Modal } from "../flowbite/components/Modal/index"; +import React, { memo, useCallback, useContext, useState } from "react"; +import { useAppDispatch, useAppSelector } from "../hooks"; +import localizationContext from "../localizationContext"; +import selectStyle from "../styles/selectStyle"; +import Select, { ActionMeta, SingleValue } from "react-select"; +import { setIsModTagPickerOpen, setTagForMod } from "../appSlice"; + +type OptionType = { + value: string; + label: string; +}; + +const options: OptionType[] = [ + "graphical", + "campaign", + "units", + "battle", + "ui", + "maps", + "overhaul", + "compilation", + "cheat", +].map((tag) => ({ value: tag, label: tag })); + +const ModTagPicker = memo(() => { + const dispatch = useAppDispatch(); + + const isModTagPickerOpen = useAppSelector((state) => state.app.isModTagPickerOpen); + const currentModToUpload = useAppSelector((state) => state.app.currentModToUpload); + + const [currentTag, setCurrentTag] = useState("graphical"); + + const onUploadMod = () => { + if (currentModToUpload) { + window.api?.uploadMod({ ...currentModToUpload, tags: ["mod", currentTag] }); + dispatch(setIsModTagPickerOpen(false)); + } + }; + + const onClose = useCallback(() => { + dispatch(setIsModTagPickerOpen(false)); + }, []); + + const onTagChange = (newValue: SingleValue, actionMeta: ActionMeta) => { + if (!newValue) return; + if (!currentModToUpload) return; + + console.log(`label: ${newValue.label}, value: ${newValue.value}, action: ${actionMeta.action}`); + if (actionMeta.action === "select-option") { + setCurrentTag(newValue.value); + dispatch(setTagForMod({ mod: currentModToUpload, tag: newValue.value })); + } + }; + + const localized: Record = useContext(localizationContext); + + return ( + <> + {currentModToUpload && isModTagPickerOpen && ( + + {localized.uploadMod} + +
+ + {localized.chooseNewModTag} + + + +
+
+
+ )} + + ); +}); +export default ModTagPicker; diff --git a/src/components/OptionsDrawer.tsx b/src/components/OptionsDrawer.tsx index be1390c..9c3731e 100644 --- a/src/components/OptionsDrawer.tsx +++ b/src/components/OptionsDrawer.tsx @@ -239,14 +239,14 @@ const OptionsDrawer = memo(() => {
diff --git a/src/index.css b/src/index.css index bdd5f31..f53192e 100644 --- a/src/index.css +++ b/src/index.css @@ -295,3 +295,7 @@ body:not(.disable-row-hover) .row:hover > div:not(.drop-ghost) { .modalDontOverflowWindowHeight { max-height: calc(100% - 2rem); } + +.modalGiveChildVisibleOverflow > :first-child { + overflow: visible; +} diff --git a/src/index.d.ts b/src/index.d.ts index da92458..61ee1c3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,6 +2,7 @@ import { PackedFile, PackCollisions } from "./packFileTypes"; import { GameFolderPaths } from "./appData"; import { api } from "./preload"; import { SupportedGames } from "./supportedGames"; +import { UgcItemVisibility } from "../node_modules/@ai-zen/steamworks.js/client.d"; export {}; declare global { @@ -38,6 +39,7 @@ declare global { subbedTime?: number; isSymbolicLink: boolean; categories?: string[]; + tags: string[]; } interface ModData { @@ -47,6 +49,8 @@ declare global { lastChanged: number; author: string; isDeleted: boolean; + subscriptionTime: number; + tags: string[]; } interface PackHeaderData { @@ -128,6 +132,8 @@ declare global { customizableMods: Record; currentGame: SupportedGames; steamCollectionsToImport: Record; + isModTagPickerOpen: boolean; + currentModToUpload: Mod | undefined; } type; @@ -338,12 +344,28 @@ declare global { indexInMods: number; loadOrder?: number; time: number; + tags: string[]; } type ToastType = "success" | "warning" | "info"; type MainWindowTab = "mods" | "enabledMods" | "categories"; + export interface WorkshopItemStatisticStringified { + numSubscriptions: string; + numFavorites: string; + numFollowers: string; + numUniqueSubscriptions: string; + numUniqueFavorites: string; + numUniqueFollowers: string; + numUniqueWebsiteViews: string; + reportScore: string; + numSecondsPlayed: string; + numPlaytimeSessions: string; + numComments: string; + numSecondsPlayedDuringTimePeriod: string; + numPlaytimeSessionsDuringTimePeriod: string; + } export interface WorkshopItemStringInsteadOfBigInt { publishedFileId: string; creatorAppId?: number; @@ -355,6 +377,9 @@ declare global { timeCreated: number; /** Time updated in unix epoch seconds format */ timeUpdated: number; + /** Time when the user added the published item to their list (not always applicable), provided in Unix epoch format (time since Jan 1st, 1970). */ + timeAddedToUserList: number; + visibility: UgcItemVisibility; banned: boolean; acceptedForUse: boolean; tags: Array; @@ -364,6 +389,7 @@ declare global { numDownvotes: number; numChildren: number; previewUrl?: string; + statistics: WorkshopItemStatisticStringified; } export interface PlayerSteamIdStringInsteadOfBigInt { diff --git a/src/index.ts b/src/index.ts index d4f713c..b370cbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { gameToProcessName, gameToSteamId } from "./supportedGames"; import psList from "ps-list"; import { exec, fork } from "child_process"; -import { app, autoUpdater, BrowserWindow, dialog, ipcMain, shell } from "electron"; +import { app, autoUpdater, BrowserWindow, dialog, ipcMain, Menu, shell } from "electron"; import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from "electron-extension-installer"; import fetch from "electron-fetch"; import isDev from "electron-is-dev"; @@ -264,6 +264,9 @@ if (!gotTheLock) { } }); + // not using a default menu + Menu.setApplicationMenu(null); + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. diff --git a/src/initialAppState.ts b/src/initialAppState.ts index dc0fc57..f210428 100644 --- a/src/initialAppState.ts +++ b/src/initialAppState.ts @@ -73,6 +73,8 @@ const initialState = { isHelpOpen: false, lastModThatWasRead: undefined, currentlyReadingMod: undefined, + isModTagPickerOpen: false, + currentModToUpload: undefined, } as AppState; export default initialState; diff --git a/src/ipcMainListeners.ts b/src/ipcMainListeners.ts index 123cfec..a475d18 100644 --- a/src/ipcMainListeners.ts +++ b/src/ipcMainListeners.ts @@ -52,6 +52,7 @@ import { format } from "date-fns"; import { sortByNameAndLoadOrder } from "./modSortingHelpers"; import { tryOpenFile } from "./utility/fileHelpers"; import steamCollectionScript from "./utility/steamCollectionScript"; +import fetch from "electron-fetch"; declare const VIEWER_WEBPACK_ENTRY: string; declare const VIEWER_PRELOAD_WEBPACK_ENTRY: string; @@ -89,7 +90,7 @@ export const registerIpcMainListeners = ( if (appData.gamesToGameFolderPaths[newGame].contentFolder) { appData.packsData = []; appData.saveSetupDone = false; - console.log("SETTING CURR GAME 2"); + console.log("Setting current game 1"); appData.currentGame = newGame; await getAllMods(); } @@ -100,7 +101,7 @@ export const registerIpcMainListeners = ( if (appData.gamesToGameFolderPaths[newGame].contentFolder) { contentFolder = appData.gamesToGameFolderPaths[newGame].contentFolder ?? ""; gamePath = appData.gamesToGameFolderPaths[newGame].gamePath ?? ""; - console.log("SETTING CURR GAME 1"); + console.log("Setting current game 2"); appData.currentGame = newGame; console.log("SENDING setAppFolderPaths", gamePath, contentFolder); // mainWindow?.webContents.send("setCurrentGameNaive", newGame); @@ -409,6 +410,7 @@ export const registerIpcMainListeners = ( isMovie: false, size: 0, isSymbolicLink: false, + tags: ["mod"], }; if (appData.packsData.every((iterPack) => iterPack.path != dataPackPath)) { console.log("READING DATA PACK"); @@ -664,7 +666,7 @@ export const registerIpcMainListeners = ( }; ipcMain.on("getAllModData", (event, ids: string[]) => { - if (isDev) return; + // if (isDev) return; fetchModData( ids.filter((id) => id !== ""), @@ -1419,7 +1421,7 @@ export const registerIpcMainListeners = ( startTime: Date.now(), } as Toast); } - updateMod(mod, response.workshopId, mod.name, true); + updateMod(mod, response.workshopId, mod.tags, mod.name, true); break; case "error": mainWindow?.webContents.send("addToast", { @@ -1436,6 +1438,7 @@ export const registerIpcMainListeners = ( const updateMod = async ( mod: Mod, workshopId: string, + tags: string[], modTitle?: string, openInSteamAfterUpdate = false ) => { @@ -1450,7 +1453,16 @@ export const registerIpcMainListeners = ( await fs.linkSync(mod.path, nodePath.join(uploadFolderPath, mod.name)); await fs.linkSync(mod.imgPath, nodePath.join(uploadFolderPath, nodePath.basename(mod.imgPath))); - const args = [gameToSteamId[appData.currentGame], "update", workshopId, uploadFolderPath, mod.imgPath]; + const args = [ + gameToSteamId[appData.currentGame], + "update", + workshopId, + uploadFolderPath, + mod.imgPath, + tags.join(";"), + ]; + console.log("UPDATING MOD:", modTitle, tags); + // return; if (modTitle) args.push(modTitle); const child = fork(nodePath.join(__dirname, "sub.js"), args, {}); child.on( @@ -1514,7 +1526,7 @@ export const registerIpcMainListeners = ( ); }; ipcMain.on("updateMod", async (event, mod: Mod, contentMod: Mod) => { - updateMod(mod, contentMod.workshopId); + updateMod(mod, contentMod.workshopId, contentMod.tags); }); ipcMain.on("fakeUpdatePack", async (event, mod: Mod) => { try { @@ -1890,6 +1902,7 @@ export const registerIpcMainListeners = ( isMovie: false, size: 0, isSymbolicLink: false, + tags: ["mod"], }; vanillaPacks.push(dataMod); } diff --git a/src/modFunctions.ts b/src/modFunctions.ts index c8fc282..d6b8e55 100644 --- a/src/modFunctions.ts +++ b/src/modFunctions.ts @@ -15,6 +15,7 @@ import { decodeHTML } from "entities"; const matchAuthorNameInSteamHtmlTag = /.*>(.+?)'s .*?<\/a>/; const matchBreadcrumbsInSteamPageHtml = /