chore: dashboard

This commit is contained in:
lencx
2023-01-24 13:23:06 +08:00
parent 1f573102d3
commit f1e528d3a7
12 changed files with 269 additions and 95 deletions

View File

@@ -140,16 +140,6 @@ pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
list list
} }
#[command]
pub fn window_reload(app: AppHandle, label: &str) {
app
.app_handle()
.get_window(label)
.unwrap()
.eval("window.location.reload()")
.unwrap();
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ModelRecord { pub struct ModelRecord {
pub cmd: String, pub cmd: String,
@@ -345,8 +335,8 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>>
"Sync Prompts", "Sync Prompts",
"ChatGPT Prompts data has been synchronized!", "ChatGPT Prompts data has been synchronized!",
); );
window_reload(app.clone(), "core"); window::window_reload(app.clone(), "core");
window_reload(app, "tray"); window::window_reload(app, "tray");
return Some(data2); return Some(data2);
} }

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
app::{cmd, window}, app::window,
conf::{self, ChatConfJson}, conf::{self, ChatConfJson},
utils, utils,
}; };
@@ -250,8 +250,8 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
.set_selected(popup_search) .set_selected(popup_search)
.unwrap(); .unwrap();
ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None).unwrap(); ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None).unwrap();
cmd::window_reload(app.clone(), "core"); window::window_reload(app.clone(), "core");
cmd::window_reload(app, "tray"); window::window_reload(app, "tray");
} }
"sync_prompts" => { "sync_prompts" => {
tauri::api::dialog::ask( tauri::api::dialog::ask(

View File

@@ -50,7 +50,12 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
} else { } else {
let app = app.handle(); let app = app.handle();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(url.clone().into())) let link = if chat_conf.dashboard {
"index.html"
} else {
&url
};
let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(link.into()))
.title("ChatGPT") .title("ChatGPT")
.resizable(true) .resizable(true)
.fullscreen(false) .fullscreen(false)
@@ -60,7 +65,7 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
main_win = main_win.hidden_title(true); main_win = main_win.hidden_title(true);
} }
if url == "https://chat.openai.com" { if url == "https://chat.openai.com" && !chat_conf.dashboard {
main_win = main_win main_win = main_win
.initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js"))

View File

@@ -97,14 +97,18 @@ pub fn control_window(handle: &tauri::AppHandle) {
let app = handle.clone(); let app = handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if app.app_handle().get_window("main").is_none() { if app.app_handle().get_window("main").is_none() {
WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into())) WindowBuilder::new(
.title("Control Center") &app,
.resizable(true) "main",
.fullscreen(false) WindowUrl::App("index.html?type=control".into()),
.inner_size(1000.0, 700.0) )
.min_inner_size(800.0, 600.0) .title("Control Center")
.build() .resizable(true)
.unwrap(); .fullscreen(false)
.inner_size(1000.0, 700.0)
.min_inner_size(800.0, 600.0)
.build()
.unwrap();
} else { } else {
let main_win = app.app_handle().get_window("main").unwrap(); let main_win = app.app_handle().get_window("main").unwrap();
main_win.show().unwrap(); main_win.show().unwrap();
@@ -112,3 +116,40 @@ pub fn control_window(handle: &tauri::AppHandle) {
} }
}); });
} }
#[tauri::command]
pub async fn wa_window(
app: tauri::AppHandle,
label: String,
title: String,
url: String,
script: Option<String>,
) {
info!("wa_window: {} :=> {}", title, url);
let win = app.get_window(&label);
if win.is_none() {
tauri::async_runtime::spawn(async move {
tauri::WindowBuilder::new(&app, label, tauri::WindowUrl::App(url.parse().unwrap()))
.initialization_script(&script.unwrap_or_default())
.initialization_script(include_str!("../scripts/core.js"))
.title(title)
.build()
.unwrap();
});
} else {
if !win.clone().unwrap().is_visible().unwrap() {
win.clone().unwrap().show().unwrap();
}
win.unwrap().set_focus().unwrap();
}
}
#[tauri::command]
pub fn window_reload(app: tauri::AppHandle, label: &str) {
app
.app_handle()
.get_window(label)
.unwrap()
.eval("window.location.reload()")
.unwrap();
}

View File

@@ -18,6 +18,7 @@ pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx";
pub const GITHUB_PROMPTS_CSV_URL: &str = pub const GITHUB_PROMPTS_CSV_URL: &str =
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv"; "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
pub const DEFAULT_CHAT_CONF: &str = r#"{ pub const DEFAULT_CHAT_CONF: &str = r#"{
"dashboard": false,
"stay_on_top": false, "stay_on_top": false,
"auto_update": "Prompt", "auto_update": "Prompt",
"theme": "Light", "theme": "Light",
@@ -33,6 +34,7 @@ pub const DEFAULT_CHAT_CONF: &str = r#"{
"ua_tray": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1" "ua_tray": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"
}"#; }"#;
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
"dashboard": false,
"stay_on_top": false, "stay_on_top": false,
"auto_update": "Prompt", "auto_update": "Prompt",
"theme": "Light", "theme": "Light",
@@ -58,6 +60,7 @@ pub struct ChatConfJson {
pub theme: String, pub theme: String,
// auto update policy, Prompt/Silent/Disable // auto update policy, Prompt/Silent/Disable
pub auto_update: String, pub auto_update: String,
pub dashboard: bool,
pub tray: bool, pub tray: bool,
pub popup_search: bool, pub popup_search: bool,
pub stay_on_top: bool, pub stay_on_top: bool,
@@ -159,15 +162,6 @@ impl ChatConfJson {
if let Some(handle) = app { if let Some(handle) = app {
tauri::api::process::restart(&handle.env()); tauri::api::process::restart(&handle.env());
// tauri::api::dialog::ask(
// handle.get_window("core").as_ref(),
// "ChatGPT Restart",
// "Whether to restart immediately?",
// move |is_restart| {
// if is_restart {
// }
// },
// );
} }
Ok(()) Ok(())

View File

@@ -7,7 +7,7 @@ mod app;
mod conf; mod conf;
mod utils; mod utils;
use app::{cmd, fs_extra, menu, setup}; use app::{cmd, fs_extra, menu, setup, window};
use conf::ChatConfJson; use conf::ChatConfJson;
use tauri::api::path; use tauri::api::path;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
@@ -73,13 +73,14 @@ async fn main() {
cmd::parse_prompt, cmd::parse_prompt,
cmd::sync_prompts, cmd::sync_prompts,
cmd::sync_user_prompts, cmd::sync_user_prompts,
cmd::window_reload,
cmd::dalle2_window, cmd::dalle2_window,
cmd::cmd_list, cmd::cmd_list,
cmd::download_list, cmd::download_list,
cmd::get_download_list, cmd::get_download_list,
cmd::get_data, cmd::get_data,
fs_extra::metadata, fs_extra::metadata,
window::window_reload,
window::wa_window,
]) ])
.setup(setup::init) .setup(setup::init)
.menu(menu::init()); .menu(menu::init());

125
src/layout/index.tsx vendored
View File

@@ -13,13 +13,18 @@ const { Content, Footer, Sider } = Layout;
export default function ChatLayout() { export default function ChatLayout() {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [isDashboard, setDashboard] = useState<any>(null);
const [appInfo, setAppInfo] = useState<Record<string, any>>({}); const [appInfo, setAppInfo] = useState<Record<string, any>>({});
const location = useLocation(); const location = useLocation();
const [menuKey, setMenuKey] = useState(location.pathname); const [menuKey, setMenuKey] = useState(location.pathname);
const go = useNavigate(); const go = useNavigate();
useEffect(() => { useEffect(() => {
if (location.search === '?type=control') {
go('/awesome');
}
setMenuKey(location.pathname); setMenuKey(location.pathname);
setDashboard(location.pathname === '/');
}, [location.pathname]); }, [location.pathname]);
useInit(async () => { useInit(async () => {
@@ -36,69 +41,75 @@ export default function ChatLayout() {
const isDark = appInfo.appTheme === 'dark'; const isDark = appInfo.appTheme === 'dark';
if (isDashboard === null) return null;
return ( return (
<ConfigProvider theme={{ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm }}> <ConfigProvider theme={{ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm }}>
<Layout style={{ minHeight: '100vh' }} hasSider> {isDashboard ? (
<Sider <Routes />
theme={isDark ? 'dark' : 'light'} ) : (
collapsible <Layout style={{ minHeight: '100vh' }} hasSider>
collapsed={collapsed} <Sider
onCollapse={(value) => setCollapsed(value)} theme={isDark ? 'dark' : 'light'}
style={{ collapsible
overflow: 'auto', collapsed={collapsed}
height: '100vh', onCollapse={(value) => setCollapsed(value)}
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
zIndex: 999,
}}
>
<div className="chat-logo">
<img src="/logo.png" />
</div>
<div className="chat-info">
<Tag>{appInfo.appName}</Tag>
<Tag>
<span style={{ marginRight: 5 }}>{appInfo.appVersion}</span>
<Tooltip title="click to check update">
<a onClick={checkAppUpdate}>
<SyncOutlined />
</a>
</Tooltip>
</Tag>
</div>
<Menu
selectedKeys={[menuKey]}
mode="inline"
theme={appInfo.appTheme === 'dark' ? 'dark' : 'light'}
inlineIndent={12}
items={menuItems}
// defaultOpenKeys={['/model']}
onClick={(i) => go(i.key)}
/>
</Sider>
<Layout
className="chat-layout"
style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}
>
<Content
className="chat-container"
style={{ style={{
overflow: 'inherit', overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
zIndex: 999,
}} }}
> >
<Routes /> <div className="chat-logo">
</Content> <img src="/logo.png" />
<Footer style={{ textAlign: 'center' }}> </div>
<a href="https://github.com/lencx/chatgpt" target="_blank"> <div className="chat-info">
ChatGPT Desktop Application <Tag>{appInfo.appName}</Tag>
</a>{' '} <Tag>
©2022 Created by lencx <span style={{ marginRight: 5 }}>{appInfo.appVersion}</span>
</Footer> <Tooltip title="click to check update">
<a onClick={checkAppUpdate}>
<SyncOutlined />
</a>
</Tooltip>
</Tag>
</div>
<Menu
selectedKeys={[menuKey]}
mode="inline"
theme={appInfo.appTheme === 'dark' ? 'dark' : 'light'}
inlineIndent={12}
items={menuItems}
// defaultOpenKeys={['/model']}
onClick={(i) => go(i.key)}
/>
</Sider>
<Layout
className="chat-layout"
style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}
>
<Content
className="chat-container"
style={{
overflow: 'inherit',
}}
>
<Routes />
</Content>
<Footer style={{ textAlign: 'center' }}>
<a href="https://github.com/lencx/chatgpt" target="_blank">
ChatGPT Desktop Application
</a>{' '}
©2022 Created by lencx
</Footer>
</Layout>
</Layout> </Layout>
</Layout> )}
</ConfigProvider> </ConfigProvider>
); );
} }

4
src/main.scss vendored
View File

@@ -15,9 +15,11 @@
} }
html, html,
body { body,
#root {
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 100%;
} }
.ant-table-selection-col, .ant-table-selection-col,

8
src/routes.tsx vendored
View File

@@ -22,6 +22,7 @@ import SyncRecord from '@/view/model/SyncRecord';
import Download from '@/view/download'; import Download from '@/view/download';
import Notes from '@/view/notes'; import Notes from '@/view/notes';
import Markdown from '@/view/markdown'; import Markdown from '@/view/markdown';
import Dashboard from '@/view/dashboard';
export type ChatRouteMetaObject = { export type ChatRouteMetaObject = {
label: string; label: string;
@@ -38,7 +39,7 @@ type ChatRouteObject = {
export const routes: Array<ChatRouteObject> = [ export const routes: Array<ChatRouteObject> = [
{ {
path: '/', path: '/awesome',
element: <Awesome />, element: <Awesome />,
meta: { meta: {
label: 'Awesome', label: 'Awesome',
@@ -121,6 +122,11 @@ export const routes: Array<ChatRouteObject> = [
icon: <InfoCircleOutlined />, icon: <InfoCircleOutlined />,
}, },
}, },
{
path: '/',
element: <Dashboard />,
hideMenu: true,
},
]; ];
type MenuItem = Required<MenuProps>['items'][number]; type MenuItem = Required<MenuProps>['items'][number];

43
src/view/dashboard/index.scss vendored Normal file
View File

@@ -0,0 +1,43 @@
.dashboard {
position: fixed;
width: calc(100% - 30px);
height: calc(100% - 30px);
overflow-y: auto;
padding: 15px;
&.dark {
background-color: #000;
}
&.has-top-dom {
padding-top: 30px;
}
.group-item {
margin-bottom: 20px;
.title {
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
}
.item {
.ant-card-body {
padding: 10px;
text-align: center;
font-weight: 500;
font-size: 14px;
}
span {
display: block;
height: 100%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

78
src/view/dashboard/index.tsx vendored Normal file
View File

@@ -0,0 +1,78 @@
import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { Row, Col, Card } from 'antd';
import { os, invoke } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import useJson from '@/hooks/useJson';
import { CHAT_AWESOME_JSON, CHAT_CONF_JSON, readJSON } from '@/utils';
import './index.scss';
export default function Dashboard() {
const { json } = useJson<Record<string, any>[]>(CHAT_AWESOME_JSON);
const [list, setList] = useState<Array<[string, Record<string, any>[]]>>([]);
const [hasClass, setClass] = useState(false);
const [theme, setTheme] = useState('');
useInit(async () => {
const getOS = await os.platform();
const conf = await readJSON(CHAT_CONF_JSON);
const appTheme = await invoke('get_theme');
setTheme(appTheme as string);
setClass(!conf?.titlebar && getOS === 'darwin');
});
useEffect(() => {
const categories = new Map();
json?.forEach((i) => {
if (!i.enable) return;
if (!categories.has(i.category)) {
categories.set(i.category, []);
}
categories.get(i?.category).push(i);
});
setList(Array.from(categories));
}, [json?.length]);
const handleLink = async (item: Record<string, any>) => {
await invoke('wa_window', {
label: btoa(item.url).replace(/[^a-zA-Z0-9]/g, ''),
title: item.title,
url: item.url,
});
};
return (
<div className={clsx('dashboard', theme, { 'has-top-dom': hasClass })}>
<div>
{list.map((i) => {
return (
<div key={i[0]} className="group-item">
<Card title={i[0]} size="small">
<Row className="list" gutter={[8, 8]}>
{i[1].map((j, idx) => {
return (
<Col
title={`${j?.title}: ${j?.url}`}
key={`${idx}_${j?.url}`}
xl={4}
md={6}
sm={8}
xs={12}
>
<Card className="item" hoverable onClick={() => handleLink(j)}>
<span>{j?.title}</span>
</Card>
</Col>
);
})}
</Row>
</Card>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -60,6 +60,9 @@ export default function General() {
return ( return (
<> <>
<Form.Item label="Dashboard" name="dashboard" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="Stay On Top" name="stay_on_top" valuePropName="checked"> <Form.Item label="Stay On Top" name="stay_on_top" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>