diff --git a/src/main/events/helpers/validators.ts b/src/main/events/helpers/validators.ts index f3c9d844..ee36bb85 100644 --- a/src/main/events/helpers/validators.ts +++ b/src/main/events/helpers/validators.ts @@ -5,7 +5,6 @@ export const downloadSourceSchema = z.object({ downloads: z.array( z.object({ title: z.string().max(255), - downloaders: z.array(z.enum(["real_debrid", "torrent"])), uris: z.array(z.string()), uploadDate: z.string().max(255), fileSize: z.string().max(255), diff --git a/src/main/index.ts b/src/main/index.ts index b2d07275..837db5f5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -73,7 +73,16 @@ app.on("browser-window-created", (_, window) => { optimizer.watchWindowShortcuts(window); }); -app.on("second-instance", (_event) => { +const handleDeepLinkPath = (uri?: string) => { + if (!uri) return; + const url = new URL(uri); + + if (url.host === "install-source") { + WindowManager.redirect(`settings${url.search}`); + } +}; + +app.on("second-instance", (_event, commandLine) => { // Someone tried to run a second instance, we should focus our window. if (WindowManager.mainWindow) { if (WindowManager.mainWindow.isMinimized()) @@ -84,16 +93,12 @@ app.on("second-instance", (_event) => { WindowManager.createMainWindow(); } - // const [, path] = commandLine.pop()?.split("://") ?? []; - // if (path) { - // WindowManager.redirect(path); - // } + handleDeepLinkPath(commandLine.pop()); }); -// app.on("open-url", (_event, url) => { -// const [, path] = url.split("://"); -// WindowManager.redirect(path); -// }); +app.on("open-url", (_event, url) => { + handleDeepLinkPath(url); +}); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 3e3eaf1c..9f4d821e 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -9,7 +9,7 @@ import { shell, } from "electron"; import { is } from "@electron-toolkit/utils"; -import { t } from "i18next"; +import i18next, { t } from "i18next"; import path from "node:path"; import icon from "@resources/icon.png?asset"; import trayIcon from "@resources/tray-icon.png?asset"; @@ -100,7 +100,13 @@ export class WindowManager { authWindow.removeMenu(); - authWindow.loadURL("https://auth.hydra.losbroxas.org/"); + const searchParams = new URLSearchParams({ + lng: i18next.language, + }); + + authWindow.loadURL( + `https://auth.hydra.losbroxas.org/?${searchParams.toString()}` + ); authWindow.once("ready-to-show", () => { authWindow.show(); diff --git a/src/renderer/src/context/index.ts b/src/renderer/src/context/index.ts index cecf5873..ded6dc3e 100644 --- a/src/renderer/src/context/index.ts +++ b/src/renderer/src/context/index.ts @@ -1 +1,2 @@ export * from "./game-details/game-details.context"; +export * from "./settings/settings.context"; diff --git a/src/renderer/src/context/settings/settings.context.tsx b/src/renderer/src/context/settings/settings.context.tsx new file mode 100644 index 00000000..dff006fc --- /dev/null +++ b/src/renderer/src/context/settings/settings.context.tsx @@ -0,0 +1,73 @@ +import { createContext, useEffect, useState } from "react"; + +import { setUserPreferences } from "@renderer/features"; +import { useAppDispatch } from "@renderer/hooks"; +import type { UserPreferences } from "@types"; +import { useSearchParams } from "react-router-dom"; + +export interface SettingsContext { + updateUserPreferences: (values: Partial) => Promise; + setCurrentCategoryIndex: React.Dispatch>; + clearSourceUrl: () => void; + sourceUrl: string | null; + currentCategoryIndex: number; +} + +export const settingsContext = createContext({ + updateUserPreferences: async () => {}, + setCurrentCategoryIndex: () => {}, + clearSourceUrl: () => {}, + sourceUrl: null, + currentCategoryIndex: 0, +}); + +const { Provider } = settingsContext; +export const { Consumer: SettingsContextConsumer } = settingsContext; + +export interface SettingsContextProviderProps { + children: React.ReactNode; +} + +export function SettingsContextProvider({ + children, +}: SettingsContextProviderProps) { + const dispatch = useAppDispatch(); + const [sourceUrl, setSourceUrl] = useState(null); + const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0); + + const [searchParams] = useSearchParams(); + const defaultSourceUrl = searchParams.get("urls"); + + useEffect(() => { + if (sourceUrl) setCurrentCategoryIndex(2); + }, [sourceUrl]); + + useEffect(() => { + if (defaultSourceUrl) { + setSourceUrl(defaultSourceUrl); + } + }, [defaultSourceUrl]); + + const clearSourceUrl = () => setSourceUrl(null); + + const updateUserPreferences = async (values: Partial) => { + await window.electron.updateUserPreferences(values); + window.electron.getUserPreferences().then((userPreferences) => { + dispatch(setUserPreferences(userPreferences)); + }); + }; + + return ( + + {children} + + ); +} diff --git a/src/renderer/src/pages/settings/add-download-source-modal.tsx b/src/renderer/src/pages/settings/add-download-source-modal.tsx index 10e497dd..98495c67 100644 --- a/src/renderer/src/pages/settings/add-download-source-modal.tsx +++ b/src/renderer/src/pages/settings/add-download-source-modal.tsx @@ -1,8 +1,9 @@ -import { useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Modal, TextField } from "@renderer/components"; import { SPACING_UNIT } from "@renderer/theme.css"; +import { settingsContext } from "@renderer/context"; interface AddDownloadSourceModalProps { visible: boolean; @@ -23,24 +24,31 @@ export function AddDownloadSourceModal({ downloadCount: number; } | null>(null); - useEffect(() => { - setValue(""); - setIsLoading(false); - setValidationResult(null); - }, [visible]); - const { t } = useTranslation("settings"); - const handleValidateDownloadSource = async () => { + const { sourceUrl } = useContext(settingsContext); + + const handleValidateDownloadSource = useCallback(async (url: string) => { setIsLoading(true); try { - const result = await window.electron.validateDownloadSource(value); + const result = await window.electron.validateDownloadSource(url); setValidationResult(result); } finally { setIsLoading(false); } - }; + }, []); + + useEffect(() => { + setValue(""); + setIsLoading(false); + setValidationResult(null); + + if (sourceUrl) { + setValue(sourceUrl); + handleValidateDownloadSource(sourceUrl); + } + }, [visible, handleValidateDownloadSource, sourceUrl]); const handleAddDownloadSource = async () => { await window.electron.addDownloadSource(value); @@ -73,7 +81,7 @@ export function AddDownloadSourceModal({ type="button" theme="outline" style={{ alignSelf: "flex-end" }} - onClick={handleValidateDownloadSource} + onClick={() => handleValidateDownloadSource(value)} disabled={isLoading || !value} > {t("validate_download_source")} diff --git a/src/renderer/src/pages/settings/settings-behavior.tsx b/src/renderer/src/pages/settings/settings-behavior.tsx index 4f8245a6..7f7aa5fd 100644 --- a/src/renderer/src/pages/settings/settings-behavior.tsx +++ b/src/renderer/src/pages/settings/settings-behavior.tsx @@ -1,22 +1,17 @@ -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import type { UserPreferences } from "@types"; - import { CheckboxField } from "@renderer/components"; import { useAppSelector } from "@renderer/hooks"; +import { settingsContext } from "@renderer/context"; -export interface SettingsBehaviorProps { - updateUserPreferences: (values: Partial) => void; -} - -export function SettingsBehavior({ - updateUserPreferences, -}: SettingsBehaviorProps) { +export function SettingsBehavior() { const userPreferences = useAppSelector( (state) => state.userPreferences.value ); + const { updateUserPreferences } = useContext(settingsContext); + const [form, setForm] = useState({ preferQuitInsteadOfHiding: false, runAtStartup: false, diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 6e12949d..8214117a 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { TextField, Button, Badge } from "@renderer/components"; import { useTranslation } from "react-i18next"; @@ -10,6 +10,7 @@ import { AddDownloadSourceModal } from "./add-download-source-modal"; import { useToast } from "@renderer/hooks"; import { DownloadSourceStatus } from "@shared"; import { SPACING_UNIT } from "@renderer/theme.css"; +import { settingsContext } from "@renderer/context"; export function SettingsDownloadSources() { const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = @@ -18,8 +19,9 @@ export function SettingsDownloadSources() { const [isSyncingDownloadSources, setIsSyncingDownloadSources] = useState(false); - const { t } = useTranslation("settings"); + const { sourceUrl, clearSourceUrl } = useContext(settingsContext); + const { t } = useTranslation("settings"); const { showSuccessToast } = useToast(); const getDownloadSources = async () => { @@ -32,6 +34,10 @@ export function SettingsDownloadSources() { getDownloadSources(); }, []); + useEffect(() => { + if (sourceUrl) setShowAddDownloadSourceModal(true); + }, [sourceUrl]); + const handleRemoveSource = async (id: number) => { await window.electron.removeDownloadSource(id); showSuccessToast(t("removed_download_source")); @@ -63,11 +69,16 @@ export function SettingsDownloadSources() { [DownloadSourceStatus.Errored]: t("download_source_errored"), }; + const handleModalClose = () => { + clearSourceUrl(); + setShowAddDownloadSourceModal(false); + }; + return ( <> setShowAddDownloadSourceModal(false)} + onClose={handleModalClose} onAddDownloadSource={handleAddDownloadSource} /> diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index fb9e459e..e09ebb11 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import ISO6391 from "iso-639-1"; import { @@ -8,27 +8,24 @@ import { SelectField, } from "@renderer/components"; import { useTranslation } from "react-i18next"; -import type { UserPreferences } from "@types"; + import { useAppSelector } from "@renderer/hooks"; import { changeLanguage } from "i18next"; import * as languageResources from "@locales"; import { orderBy } from "lodash-es"; +import { settingsContext } from "@renderer/context"; interface LanguageOption { option: string; nativeName: string; } -export interface SettingsGeneralProps { - updateUserPreferences: (values: Partial) => void; -} - -export function SettingsGeneral({ - updateUserPreferences, -}: SettingsGeneralProps) { +export function SettingsGeneral() { const { t } = useTranslation("settings"); + const { updateUserPreferences } = useContext(settingsContext); + const userPreferences = useAppSelector( (state) => state.userPreferences.value ); diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx index 3dc4186d..35804664 100644 --- a/src/renderer/src/pages/settings/settings-real-debrid.tsx +++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx @@ -1,26 +1,23 @@ -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Button, CheckboxField, Link, TextField } from "@renderer/components"; import * as styles from "./settings-real-debrid.css"; -import type { UserPreferences } from "@types"; + import { useAppSelector, useToast } from "@renderer/hooks"; import { SPACING_UNIT } from "@renderer/theme.css"; +import { settingsContext } from "@renderer/context"; const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken"; -export interface SettingsRealDebridProps { - updateUserPreferences: (values: Partial) => void; -} - -export function SettingsRealDebrid({ - updateUserPreferences, -}: SettingsRealDebridProps) { +export function SettingsRealDebrid() { const userPreferences = useAppSelector( (state) => state.userPreferences.value ); + const { updateUserPreferences } = useContext(settingsContext); + const [isLoading, setIsLoading] = useState(false); const [form, setForm] = useState({ useRealDebrid: false, diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index c4b5964b..2718470f 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -1,21 +1,20 @@ -import { useState } from "react"; import { Button } from "@renderer/components"; import * as styles from "./settings.css"; import { useTranslation } from "react-i18next"; -import { UserPreferences } from "@types"; import { SettingsRealDebrid } from "./settings-real-debrid"; import { SettingsGeneral } from "./settings-general"; import { SettingsBehavior } from "./settings-behavior"; -import { useAppDispatch } from "@renderer/hooks"; -import { setUserPreferences } from "@renderer/features"; + import { SettingsDownloadSources } from "./settings-download-sources"; +import { + SettingsContextConsumer, + SettingsContextProvider, +} from "@renderer/context"; export function Settings() { const { t } = useTranslation("settings"); - const dispatch = useAppDispatch(); - const categories = [ t("general"), t("behavior"), @@ -23,57 +22,50 @@ export function Settings() { "Real-Debrid", ]; - const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0); - - const handleUpdateUserPreferences = async ( - values: Partial - ) => { - await window.electron.updateUserPreferences(values); - window.electron.getUserPreferences().then((userPreferences) => { - dispatch(setUserPreferences(userPreferences)); - }); - }; - - const renderCategory = () => { - if (currentCategoryIndex === 0) { - return ( - - ); - } - - if (currentCategoryIndex === 1) { - return ( - - ); - } - - if (currentCategoryIndex === 2) { - return ; - } - - return ( - - ); - }; - return ( -
-
-
- {categories.map((category, index) => ( - - ))} -
+ + + {({ currentCategoryIndex, setCurrentCategoryIndex }) => { + const renderCategory = () => { + if (currentCategoryIndex === 0) { + return ; + } -

{categories[currentCategoryIndex]}

- {renderCategory()} -
-
+ if (currentCategoryIndex === 1) { + return ; + } + + if (currentCategoryIndex === 2) { + return ; + } + + return ; + }; + + return ( +
+
+
+ {categories.map((category, index) => ( + + ))} +
+ +

{categories[currentCategoryIndex]}

+ {renderCategory()} +
+
+ ); + }} + + ); }