mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-30 06:11:03 +00:00
354 lines
9.4 KiB
TypeScript
354 lines
9.4 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
|
import { WorkWonders } from "workwonders-sdk";
|
|
import {
|
|
useAppDispatch,
|
|
useAppSelector,
|
|
useDownload,
|
|
useLibrary,
|
|
useToast,
|
|
useUserDetails,
|
|
} from "@renderer/hooks";
|
|
import { useDownloadOptionsListener } from "@renderer/hooks/use-download-options-listener";
|
|
|
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
|
import {
|
|
setUserPreferences,
|
|
toggleDraggingDisabled,
|
|
closeToast,
|
|
setUserDetails,
|
|
setProfileBackground,
|
|
setGameRunning,
|
|
setExtractionProgress,
|
|
clearExtraction,
|
|
} from "@renderer/features";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useSubscription } from "./hooks/use-subscription";
|
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
|
import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal";
|
|
|
|
import {
|
|
injectCustomCss,
|
|
removeCustomCss,
|
|
getAchievementSoundUrl,
|
|
getAchievementSoundVolume,
|
|
} from "./helpers";
|
|
import { levelDBService } from "./services/leveldb.service";
|
|
import type { UserPreferences } from "@types";
|
|
import "./app.scss";
|
|
|
|
export interface AppProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function App() {
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const { updateLibrary, library } = useLibrary();
|
|
|
|
// Listen for new download options updates
|
|
useDownloadOptionsListener();
|
|
|
|
const { t } = useTranslation("app");
|
|
|
|
const { clearDownload, setLastPacket } = useDownload();
|
|
|
|
const workwondersRef = useRef<WorkWonders | null>(null);
|
|
|
|
const {
|
|
hasActiveSubscription,
|
|
fetchUserDetails,
|
|
updateUserDetails,
|
|
clearUserDetails,
|
|
} = useUserDetails();
|
|
|
|
const { hideHydraCloudModal, isHydraCloudModalVisible, hydraCloudFeature } =
|
|
useSubscription();
|
|
|
|
const dispatch = useAppDispatch();
|
|
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const draggingDisabled = useAppSelector(
|
|
(state) => state.window.draggingDisabled
|
|
);
|
|
|
|
const toast = useAppSelector((state) => state.toast);
|
|
|
|
const { showSuccessToast } = useToast();
|
|
|
|
const [showArchiveDeletionModal, setShowArchiveDeletionModal] =
|
|
useState(false);
|
|
const [archivePaths, setArchivePaths] = useState<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
levelDBService.get("userPreferences", null, "json"),
|
|
updateLibrary(),
|
|
]).then(([preferences]) => {
|
|
dispatch(setUserPreferences(preferences as UserPreferences | null));
|
|
});
|
|
}, [navigate, location.pathname, dispatch, updateLibrary]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = window.electron.onDownloadProgress(
|
|
(downloadProgress) => {
|
|
if (downloadProgress?.progress === 1) {
|
|
clearDownload();
|
|
updateLibrary();
|
|
return;
|
|
}
|
|
|
|
setLastPacket(downloadProgress);
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, [clearDownload, setLastPacket, updateLibrary]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = window.electron.onHardDelete(() => {
|
|
updateLibrary();
|
|
});
|
|
|
|
return () => unsubscribe();
|
|
}, [updateLibrary]);
|
|
|
|
const setupWorkWonders = useCallback(
|
|
async (token?: string, locale?: string) => {
|
|
if (workwondersRef.current) return;
|
|
|
|
const possibleLocales = ["en", "pt", "ru"];
|
|
|
|
const parsedLocale =
|
|
possibleLocales.find((l) => l === locale?.slice(0, 2)) ?? "en";
|
|
|
|
workwondersRef.current = new WorkWonders();
|
|
await workwondersRef.current.init({
|
|
organization: "hydra",
|
|
token,
|
|
locale: parsedLocale,
|
|
});
|
|
|
|
await workwondersRef.current.changelog.initChangelogWidget();
|
|
workwondersRef.current.changelog.initChangelogWidgetMini();
|
|
|
|
if (token) {
|
|
workwondersRef.current.feedback.initFeedbackWidget();
|
|
}
|
|
},
|
|
[workwondersRef]
|
|
);
|
|
|
|
const setupExternalResources = useCallback(async () => {
|
|
const cachedUserDetails = window.localStorage.getItem("userDetails");
|
|
|
|
if (cachedUserDetails) {
|
|
const { profileBackground, ...userDetails } =
|
|
JSON.parse(cachedUserDetails);
|
|
|
|
dispatch(setUserDetails(userDetails));
|
|
dispatch(setProfileBackground(profileBackground));
|
|
}
|
|
|
|
const userPreferences = await window.electron.getUserPreferences();
|
|
const userDetails = await fetchUserDetails().catch(() => null);
|
|
|
|
if (userDetails) {
|
|
updateUserDetails(userDetails);
|
|
}
|
|
|
|
setupWorkWonders(userDetails?.workwondersJwt, userPreferences?.language);
|
|
|
|
if (!document.getElementById("external-resources")) {
|
|
const $script = document.createElement("script");
|
|
$script.id = "external-resources";
|
|
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`;
|
|
document.head.appendChild($script);
|
|
}
|
|
}, [fetchUserDetails, updateUserDetails, dispatch, setupWorkWonders]);
|
|
|
|
useEffect(() => {
|
|
setupExternalResources();
|
|
}, [setupExternalResources]);
|
|
|
|
const onSignIn = useCallback(() => {
|
|
fetchUserDetails().then((response) => {
|
|
if (response) {
|
|
updateUserDetails(response);
|
|
showSuccessToast(t("successfully_signed_in"));
|
|
}
|
|
});
|
|
}, [fetchUserDetails, t, showSuccessToast, updateUserDetails]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
|
|
if (gamesRunning.length) {
|
|
const lastGame = gamesRunning[gamesRunning.length - 1];
|
|
const libraryGame = library.find(
|
|
(library) => library.id === lastGame.id
|
|
);
|
|
|
|
if (libraryGame) {
|
|
dispatch(
|
|
setGameRunning({
|
|
...libraryGame,
|
|
sessionDurationInMillis: lastGame.sessionDurationInMillis,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
dispatch(setGameRunning(null));
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, [dispatch, library]);
|
|
|
|
useEffect(() => {
|
|
const listeners = [
|
|
window.electron.onSignIn(onSignIn),
|
|
window.electron.onLibraryBatchComplete(() => {
|
|
updateLibrary();
|
|
}),
|
|
window.electron.onSignOut(() => clearUserDetails()),
|
|
window.electron.onExtractionProgress((shop, objectId, progress) => {
|
|
dispatch(setExtractionProgress({ shop, objectId, progress }));
|
|
}),
|
|
window.electron.onExtractionComplete(() => {
|
|
dispatch(clearExtraction());
|
|
updateLibrary();
|
|
}),
|
|
window.electron.onArchiveDeletionPrompt((paths) => {
|
|
setArchivePaths(paths);
|
|
setShowArchiveDeletionModal(true);
|
|
}),
|
|
];
|
|
|
|
return () => {
|
|
listeners.forEach((unsubscribe) => unsubscribe());
|
|
};
|
|
}, [onSignIn, updateLibrary, clearUserDetails, dispatch]);
|
|
|
|
useEffect(() => {
|
|
if (contentRef.current) contentRef.current.scrollTop = 0;
|
|
workwondersRef.current?.notifyUrlChange();
|
|
}, [location.pathname, location.search]);
|
|
|
|
useEffect(() => {
|
|
new MutationObserver(() => {
|
|
const modal = document.body.querySelector("[data-hydra-dialog]");
|
|
|
|
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
|
}).observe(document.body, {
|
|
attributes: false,
|
|
childList: true,
|
|
});
|
|
}, [dispatch, draggingDisabled]);
|
|
|
|
const loadAndApplyTheme = useCallback(async () => {
|
|
const allThemes = (await levelDBService.values("themes")) as {
|
|
isActive?: boolean;
|
|
code?: string;
|
|
}[];
|
|
const activeTheme = allThemes.find((theme) => theme.isActive);
|
|
if (activeTheme?.code) {
|
|
injectCustomCss(activeTheme.code);
|
|
} else {
|
|
removeCustomCss();
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadAndApplyTheme();
|
|
}, [loadAndApplyTheme]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = window.electron.onCustomThemeUpdated(() => {
|
|
loadAndApplyTheme();
|
|
});
|
|
|
|
return () => unsubscribe();
|
|
}, [loadAndApplyTheme]);
|
|
|
|
const playAudio = useCallback(async () => {
|
|
const soundUrl = await getAchievementSoundUrl();
|
|
const volume = await getAchievementSoundVolume();
|
|
const audio = new Audio(soundUrl);
|
|
audio.volume = volume;
|
|
audio.play();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = window.electron.onAchievementUnlocked(() => {
|
|
playAudio();
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, [playAudio]);
|
|
|
|
const handleToastClose = useCallback(() => {
|
|
dispatch(closeToast());
|
|
}, [dispatch]);
|
|
|
|
return (
|
|
<>
|
|
{window.electron.platform === "win32" && (
|
|
<div className="title-bar">
|
|
<h4>
|
|
Hydra
|
|
{hasActiveSubscription && (
|
|
<span className="title-bar__cloud-text"> Cloud</span>
|
|
)}
|
|
</h4>
|
|
</div>
|
|
)}
|
|
|
|
<Toast
|
|
visible={toast.visible}
|
|
title={toast.title}
|
|
message={toast.message}
|
|
type={toast.type}
|
|
onClose={handleToastClose}
|
|
duration={toast.duration}
|
|
/>
|
|
|
|
<HydraCloudModal
|
|
visible={isHydraCloudModalVisible}
|
|
onClose={hideHydraCloudModal}
|
|
feature={hydraCloudFeature}
|
|
/>
|
|
|
|
<ArchiveDeletionModal
|
|
visible={showArchiveDeletionModal}
|
|
archivePaths={archivePaths}
|
|
onClose={() => setShowArchiveDeletionModal(false)}
|
|
/>
|
|
|
|
<main>
|
|
<Sidebar />
|
|
|
|
<article className="container">
|
|
<Header />
|
|
|
|
<section
|
|
ref={contentRef}
|
|
id="scrollableDiv"
|
|
className="container__content"
|
|
>
|
|
<Outlet />
|
|
</section>
|
|
</article>
|
|
</main>
|
|
|
|
<BottomPanel />
|
|
</>
|
|
);
|
|
}
|