@@ -33,6 +33,7 @@ export const AI_WORKER_ENDPOINT = "https://ai-worker.enjoy.bot";
|
||||
|
||||
export const WEB_API_URL = "https://enjoy.bot";
|
||||
export const WS_URL = "wss://enjoy.bot";
|
||||
export const DISCUSS_URL = "https://discuss.enjoy.bot";
|
||||
|
||||
export const DOWNLOAD_URL = "https://1000h.org/enjoy-app/install.html";
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import settings from "@main/settings";
|
||||
import downloader from "@main/downloader";
|
||||
import fs from "fs-extra";
|
||||
import log from "@main/logger";
|
||||
import { REPO_URL, WS_URL } from "@/constants";
|
||||
import { DISCUSS_URL, REPO_URL, WEB_API_URL, WS_URL } from "@/constants";
|
||||
import { AudibleProvider, TedProvider, YoutubeProvider } from "@main/providers";
|
||||
import Ffmpeg from "@main/ffmpeg";
|
||||
import { Waveform } from "./waveform";
|
||||
@@ -238,6 +238,108 @@ main.init = async () => {
|
||||
view.webContents.loadURL(url);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"view-load-community",
|
||||
(
|
||||
event,
|
||||
bounds: { x: number; y: number; width: number; height: number },
|
||||
options?: {
|
||||
navigatable?: boolean;
|
||||
accessToken?: string;
|
||||
url?: string;
|
||||
ssoUrl?: string;
|
||||
}
|
||||
) => {
|
||||
const {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = mainWindow.getBounds().width,
|
||||
height = mainWindow.getBounds().height,
|
||||
} = bounds;
|
||||
const { navigatable = false, accessToken, url = `${DISCUSS_URL}/login`, ssoUrl = `${WEB_API_URL}/discourse/sso` } = options || {};
|
||||
|
||||
logger.debug("view-load-community", url);
|
||||
const view = new WebContentsView();
|
||||
mainWindow.contentView.addChildView(view);
|
||||
|
||||
view.setBounds({
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
});
|
||||
|
||||
view.webContents.on("did-navigate", (_event, url) => {
|
||||
event.sender.send("view-on-state", {
|
||||
state: "did-navigate",
|
||||
url,
|
||||
});
|
||||
});
|
||||
view.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, _errorCode, errrorDescription, validatedURL) => {
|
||||
event.sender.send("view-on-state", {
|
||||
state: "did-fail-load",
|
||||
error: errrorDescription,
|
||||
url: validatedURL,
|
||||
});
|
||||
(view.webContents as any).destroy();
|
||||
mainWindow.contentView.removeChildView(view);
|
||||
}
|
||||
);
|
||||
|
||||
view.webContents.on("will-redirect", (details) => {
|
||||
const { url } = details;
|
||||
event.sender.send("view-on-state", {
|
||||
state: "will-redirect",
|
||||
url,
|
||||
});
|
||||
// Login via SSO
|
||||
if (url.includes(ssoUrl)) {
|
||||
details.preventDefault();
|
||||
// Auto login using access token
|
||||
view.webContents.loadURL(url, {
|
||||
extraHeaders: `Authorization: Bearer ${accessToken}\n`,
|
||||
});
|
||||
logger.debug("loading", url, "accessToken:", accessToken);
|
||||
} else {
|
||||
logger.debug("will-redirect", url);
|
||||
}
|
||||
});
|
||||
|
||||
view.webContents.on("will-navigate", (details) => {
|
||||
const { url } = details;
|
||||
event.sender.send("view-on-state", {
|
||||
state: "will-navigate",
|
||||
url,
|
||||
});
|
||||
|
||||
logger.debug("will-navigate", url);
|
||||
|
||||
// Open in browser if not navigatable
|
||||
if (!navigatable) {
|
||||
logger.debug("prevent navigation", url);
|
||||
details.preventDefault();
|
||||
shell.openExternal(url);
|
||||
}
|
||||
});
|
||||
view.webContents.loadURL(url);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle("view-resize", (_event, bounds: { x: number; y: number; width: number; height: number }) => {
|
||||
logger.debug("view-resize", bounds);
|
||||
const view = mainWindow.contentView.children[0];
|
||||
if (!view) return;
|
||||
|
||||
view.setBounds({
|
||||
x: Math.round(bounds.x),
|
||||
y: Math.round(bounds.y),
|
||||
width: Math.round(bounds.width),
|
||||
height: Math.round(bounds.height),
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("view-remove", () => {
|
||||
logger.debug("view-remove");
|
||||
|
||||
@@ -186,6 +186,21 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
scrape: (url: string) => {
|
||||
return ipcRenderer.invoke("view-scrape", url);
|
||||
},
|
||||
resize: (bounds: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
return ipcRenderer.invoke("view-resize", bounds);
|
||||
},
|
||||
loadCommunity: (
|
||||
url: string,
|
||||
bounds: { x: number; y: number; width: number; height: number },
|
||||
options?: { navigatable?: boolean; accessToken?: string }
|
||||
) => {
|
||||
return ipcRenderer.invoke("view-load-community", url, bounds, options);
|
||||
},
|
||||
onViewState: (
|
||||
callback: (
|
||||
event: IpcRendererEvent,
|
||||
|
||||
@@ -123,6 +123,15 @@ export const Sidebar = (props: {
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
|
||||
<SidebarItem
|
||||
href="/community"
|
||||
label={t("sidebar.community")}
|
||||
tooltip={t("sidebar.community")}
|
||||
active={activeTab.startsWith("/community")}
|
||||
Icon={UsersRoundIcon}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SidebarItem
|
||||
@@ -346,13 +355,6 @@ const SidebarHeader = (props: { isCollapsed: boolean }) => {
|
||||
<span>{t("sidebar.profile")}</span>
|
||||
<UserIcon className="size-4 ml-auto" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => navigate("/community")}
|
||||
>
|
||||
<span>{t("sidebar.community")}</span>
|
||||
<UsersRoundIcon className="size-4 ml-auto" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => setDisplayDepositDialog(true)}
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
import {
|
||||
Button,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsContent,
|
||||
TabsTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import { UsersRankings, Posts } from "@renderer/components";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import debounce from "lodash/debounce";
|
||||
import { DISCUSS_URL, WEB_API_URL } from "@/constants";
|
||||
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { EnjoyApp, user, webApi } = useContext(AppSettingsProviderContext);
|
||||
|
||||
return (
|
||||
<div className="min-h-full px-4 lg:px-8 py-6 max-w-screen-md mx-auto">
|
||||
<Tabs defaultValue="square">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="square">{t("square")}</TabsTrigger>
|
||||
<TabsTrigger value="rankings">{t("rankings")}</TabsTrigger>
|
||||
</TabsList>
|
||||
const loadCommunity = async () => {
|
||||
let url = `${DISCUSS_URL}/login`;
|
||||
let ssoUrl = `${WEB_API_URL}/discourse/sso`;
|
||||
|
||||
<TabsContent value="square">
|
||||
<Posts />
|
||||
</TabsContent>
|
||||
try {
|
||||
const { discussUrl, discussSsoUrl } = await webApi.config("discuss");
|
||||
if (discussUrl) {
|
||||
url = discussUrl;
|
||||
}
|
||||
if (discussSsoUrl) {
|
||||
ssoUrl = discussSsoUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
<TabsContent value="rankings">
|
||||
<UsersRankings />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
const { x, y, width, height } =
|
||||
containerRef.current.getBoundingClientRect();
|
||||
EnjoyApp.view.loadCommunity(
|
||||
{ x, y, width, height },
|
||||
{ navigatable: false, accessToken: user?.accessToken, url, ssoUrl }
|
||||
);
|
||||
};
|
||||
|
||||
const resize = debounce(() => {
|
||||
const { x, y, width, height } =
|
||||
containerRef.current.getBoundingClientRect();
|
||||
EnjoyApp.view.resize({ x, y, width, height });
|
||||
}, 100);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
loadCommunity();
|
||||
const observer = new ResizeObserver(() => {
|
||||
resize();
|
||||
});
|
||||
observer.observe(containerRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
EnjoyApp.view.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} className="w-full h-full"></div>;
|
||||
};
|
||||
|
||||
15
enjoy/src/types/enjoy-app.d.ts
vendored
15
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -91,6 +91,21 @@ type EnjoyAppType = {
|
||||
hide: () => Promise<void>;
|
||||
remove: () => Promise<void>;
|
||||
scrape: (url: string) => Promise<void>;
|
||||
loadCommunity: (
|
||||
bounds: { x: number; y: number; width: number; height: number },
|
||||
options?: {
|
||||
navigatable?: boolean;
|
||||
accessToken?: string;
|
||||
url?: string;
|
||||
ssoUrl?: string;
|
||||
}
|
||||
) => Promise<void>;
|
||||
resize: (bounds: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => Promise<void>;
|
||||
onViewState: (
|
||||
callback: (
|
||||
event,
|
||||
|
||||
Reference in New Issue
Block a user