Merge pull request #631 from hydralauncher/feature/deep-link-to-import-sources

feat: adding deep linking to import sources
This commit is contained in:
Chubby Granny Chaser 2024-06-22 20:33:24 +01:00 committed by GitHub
commit 16a4680029
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 194 additions and 110 deletions

View File

@ -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),

View File

@ -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

View File

@ -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();

View File

@ -1 +1,2 @@
export * from "./game-details/game-details.context";
export * from "./settings/settings.context";

View 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>
);
}

View File

@ -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")}

View File

@ -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,

View File

@ -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}
/>

View File

@ -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
);

View File

@ -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,

View File

@ -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>
);
}