mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
feat: adding deep linking to import sources
This commit is contained in:
parent
fbcacd7c39
commit
6d3b04fc3c
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -1 +1,2 @@
|
||||
export * from "./game-details/game-details.context";
|
||||
export * from "./settings/settings.context";
|
||||
|
73
src/renderer/src/context/settings/settings.context.tsx
Normal file
73
src/renderer/src/context/settings/settings.context.tsx
Normal file
@ -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<UserPreferences>) => Promise<void>;
|
||||
setCurrentCategoryIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
clearSourceUrl: () => void;
|
||||
sourceUrl: string | null;
|
||||
currentCategoryIndex: number;
|
||||
}
|
||||
|
||||
export const settingsContext = createContext<SettingsContext>({
|
||||
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<string | null>(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<UserPreferences>) => {
|
||||
await window.electron.updateUserPreferences(values);
|
||||
window.electron.getUserPreferences().then((userPreferences) => {
|
||||
dispatch(setUserPreferences(userPreferences));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
updateUserPreferences,
|
||||
setCurrentCategoryIndex,
|
||||
clearSourceUrl,
|
||||
currentCategoryIndex,
|
||||
sourceUrl,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
@ -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")}
|
||||
|
@ -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<UserPreferences>) => 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,
|
||||
|
@ -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 (
|
||||
<>
|
||||
<AddDownloadSourceModal
|
||||
visible={showAddDownloadSourceModal}
|
||||
onClose={() => setShowAddDownloadSourceModal(false)}
|
||||
onClose={handleModalClose}
|
||||
onAddDownloadSource={handleAddDownloadSource}
|
||||
/>
|
||||
|
||||
|
@ -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<UserPreferences>) => void;
|
||||
}
|
||||
|
||||
export function SettingsGeneral({
|
||||
updateUserPreferences,
|
||||
}: SettingsGeneralProps) {
|
||||
export function SettingsGeneral() {
|
||||
const { t } = useTranslation("settings");
|
||||
|
||||
const { updateUserPreferences } = useContext(settingsContext);
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
(state) => state.userPreferences.value
|
||||
);
|
||||
|
@ -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<UserPreferences>) => 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,
|
||||
|
@ -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<UserPreferences>
|
||||
) => {
|
||||
await window.electron.updateUserPreferences(values);
|
||||
window.electron.getUserPreferences().then((userPreferences) => {
|
||||
dispatch(setUserPreferences(userPreferences));
|
||||
});
|
||||
};
|
||||
|
||||
const renderCategory = () => {
|
||||
if (currentCategoryIndex === 0) {
|
||||
return (
|
||||
<SettingsGeneral updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 1) {
|
||||
return (
|
||||
<SettingsBehavior updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 2) {
|
||||
return <SettingsDownloadSources />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsRealDebrid updateUserPreferences={handleUpdateUserPreferences} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={currentCategoryIndex === index ? "primary" : "outline"}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
<SettingsContextProvider>
|
||||
<SettingsContextConsumer>
|
||||
{({ currentCategoryIndex, setCurrentCategoryIndex }) => {
|
||||
const renderCategory = () => {
|
||||
if (currentCategoryIndex === 0) {
|
||||
return <SettingsGeneral />;
|
||||
}
|
||||
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
</section>
|
||||
if (currentCategoryIndex === 1) {
|
||||
return <SettingsBehavior />;
|
||||
}
|
||||
|
||||
if (currentCategoryIndex === 2) {
|
||||
return <SettingsDownloadSources />;
|
||||
}
|
||||
|
||||
return <SettingsRealDebrid />;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<section className={styles.settingsCategories}>
|
||||
{categories.map((category, index) => (
|
||||
<Button
|
||||
key={category}
|
||||
theme={
|
||||
currentCategoryIndex === index ? "primary" : "outline"
|
||||
}
|
||||
onClick={() => setCurrentCategoryIndex(index)}
|
||||
>
|
||||
{category}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<h2>{categories[currentCategoryIndex]}</h2>
|
||||
{renderCategory()}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}}
|
||||
</SettingsContextConsumer>
|
||||
</SettingsContextProvider>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user