Skip to content

Commit 1d14396

Browse files
committed
feat(gui): add context menu and improve GUI interface
1 parent 1b480e8 commit 1d14396

File tree

7 files changed

+335
-75
lines changed

7 files changed

+335
-75
lines changed

packages/core/src/command/actions/gui.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import process from 'node:process'
12
import moduleData from '@/question/module/module.data'
23
import pluginData from '@/question/plugin/plugin.data'
34
import { templateList } from '@/question/template/template.data'
@@ -25,7 +26,9 @@ export function actionGuiCLI() {
2526
})
2627

2728
const [command, ..._args] = fullCustomCommand.split(' ')
28-
const { error, stdout } = sync(command, [..._args, '--input', input], {
29+
30+
process.env.CREATE_UNI_GUI_INPUT = input
31+
const { error, stdout } = sync(command, [..._args], {
2932
stdio: 'pipe',
3033
})
3134

@@ -35,7 +38,6 @@ export function actionGuiCLI() {
3538
let data: any
3639
if (stdout.length > 0) {
3740
const data_string = stdout.toString()
38-
console.log(data_string)
3941
try {
4042
const _data = JSON.parse(data_string)
4143
if (_data.useTemplate) {
@@ -48,10 +50,13 @@ export function actionGuiCLI() {
4850
data = _data
4951
}
5052
}
51-
catch (e) {
52-
throw new Error(`Error parsing JSON: ${e}`)
53+
catch {
54+
process.exit(0)
5355
}
5456
}
5557

58+
if (!data?.projectName) {
59+
process.exit(0)
60+
}
5661
return data
5762
}

packages/gui/src/lib.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use wry::{dpi::LogicalSize, http::Request, WebViewBuilder};
1414

1515
use rfd::FileDialog;
1616
use std::env;
17-
use std::io::{self, Read};
1817
use std::path::PathBuf;
1918
use webbrowser::{open_browser, Browser::Default};
2019

@@ -27,14 +26,14 @@ enum UserEvent {
2726
#[napi]
2827
pub fn create_webview() -> Result<()> {
2928
let current_dir: PathBuf = env::current_dir().expect("Unable to get current working directory");
30-
let mut input = String::new();
31-
32-
let args: Vec<String> = env::args().collect();
33-
if let Some(input_value) = args.iter().position(|x| x == "--input").and_then(|i| args.get(i + 1)) {
34-
input = input_value.to_string();
35-
} else {
36-
println!("No input provided.");
37-
}
29+
let input = match env::var("CREATE_UNI_GUI_INPUT") {
30+
Ok(val) => {
31+
val
32+
},
33+
Err(_) => {
34+
String::from("default_value")
35+
}
36+
};
3837

3938
let current_dir_str = current_dir.to_str().unwrap_or("");
4039
let escaped_current_dir_str = current_dir_str.replace("\\", "\\\\");
@@ -74,6 +73,9 @@ pub fn create_webview() -> Result<()> {
7473
let url = req.next().unwrap();
7574
open_browser(Default, url).unwrap();
7675
}
76+
"close" => {
77+
let _ = proxy.send_event(UserEvent::CloseWindow);
78+
}
7779
"install" => {
7880
let message = req.next().unwrap();
7981
println!("{}", message);
@@ -108,7 +110,9 @@ pub fn create_webview() -> Result<()> {
108110
event: WindowEvent::CloseRequested,
109111
..
110112
}
111-
| Event::UserEvent(UserEvent::CloseWindow) => *control_flow = ControlFlow::Exit,
113+
| Event::UserEvent(UserEvent::CloseWindow) => {
114+
*control_flow = ControlFlow::Exit
115+
},
112116

113117
Event::UserEvent(e) => match e {
114118
UserEvent::FilePath => {

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
},
1010
"dependencies": {
1111
"@radix-ui/react-checkbox": "^1.1.3",
12+
"@radix-ui/react-context-menu": "^2.2.4",
1213
"@radix-ui/react-dialog": "^1.1.4",
1314
"@radix-ui/react-dropdown-menu": "^2.1.4",
1415
"@radix-ui/react-label": "^2.1.1",
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { cn } from '@/lib/utils'
2+
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
3+
import { Check, ChevronRight, Circle } from 'lucide-react'
4+
5+
import * as React from 'react'
6+
7+
const ContextMenu = ContextMenuPrimitive.Root
8+
9+
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10+
11+
const ContextMenuGroup = ContextMenuPrimitive.Group
12+
13+
const ContextMenuPortal = ContextMenuPrimitive.Portal
14+
15+
const ContextMenuSub = ContextMenuPrimitive.Sub
16+
17+
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18+
19+
const ContextMenuSubTrigger = React.forwardRef<
20+
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22+
inset?: boolean
23+
}
24+
>(({ className, inset, children, ...props }, ref) => (
25+
<ContextMenuPrimitive.SubTrigger
26+
ref={ref}
27+
className={cn(
28+
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
29+
inset && 'pl-8',
30+
className,
31+
)}
32+
{...props}
33+
>
34+
{children}
35+
<ChevronRight className="ml-auto h-4 w-4" />
36+
</ContextMenuPrimitive.SubTrigger>
37+
))
38+
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
39+
40+
const ContextMenuSubContent = React.forwardRef<
41+
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43+
>(({ className, ...props }, ref) => (
44+
<ContextMenuPrimitive.SubContent
45+
ref={ref}
46+
className={cn(
47+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
48+
className,
49+
)}
50+
{...props}
51+
/>
52+
))
53+
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
54+
55+
const ContextMenuContent = React.forwardRef<
56+
React.ElementRef<typeof ContextMenuPrimitive.Content>,
57+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58+
>(({ className, ...props }, ref) => (
59+
<ContextMenuPrimitive.Portal>
60+
<ContextMenuPrimitive.Content
61+
ref={ref}
62+
className={cn(
63+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
64+
className,
65+
)}
66+
{...props}
67+
/>
68+
</ContextMenuPrimitive.Portal>
69+
))
70+
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
71+
72+
const ContextMenuItem = React.forwardRef<
73+
React.ElementRef<typeof ContextMenuPrimitive.Item>,
74+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75+
inset?: boolean
76+
}
77+
>(({ className, inset, ...props }, ref) => (
78+
<ContextMenuPrimitive.Item
79+
ref={ref}
80+
className={cn(
81+
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
82+
inset && 'pl-8',
83+
className,
84+
)}
85+
{...props}
86+
/>
87+
))
88+
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
89+
90+
const ContextMenuCheckboxItem = React.forwardRef<
91+
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93+
>(({ className, children, checked, ...props }, ref) => (
94+
<ContextMenuPrimitive.CheckboxItem
95+
ref={ref}
96+
className={cn(
97+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
98+
className,
99+
)}
100+
checked={checked}
101+
{...props}
102+
>
103+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104+
<ContextMenuPrimitive.ItemIndicator>
105+
<Check className="h-4 w-4" />
106+
</ContextMenuPrimitive.ItemIndicator>
107+
</span>
108+
{children}
109+
</ContextMenuPrimitive.CheckboxItem>
110+
))
111+
ContextMenuCheckboxItem.displayName
112+
= ContextMenuPrimitive.CheckboxItem.displayName
113+
114+
const ContextMenuRadioItem = React.forwardRef<
115+
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
116+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
117+
>(({ className, children, ...props }, ref) => (
118+
<ContextMenuPrimitive.RadioItem
119+
ref={ref}
120+
className={cn(
121+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
122+
className,
123+
)}
124+
{...props}
125+
>
126+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127+
<ContextMenuPrimitive.ItemIndicator>
128+
<Circle className="h-4 w-4 fill-current" />
129+
</ContextMenuPrimitive.ItemIndicator>
130+
</span>
131+
{children}
132+
</ContextMenuPrimitive.RadioItem>
133+
))
134+
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
135+
136+
const ContextMenuLabel = React.forwardRef<
137+
React.ElementRef<typeof ContextMenuPrimitive.Label>,
138+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
139+
inset?: boolean
140+
}
141+
>(({ className, inset, ...props }, ref) => (
142+
<ContextMenuPrimitive.Label
143+
ref={ref}
144+
className={cn(
145+
'px-2 py-1.5 text-sm font-semibold text-foreground',
146+
inset && 'pl-8',
147+
className,
148+
)}
149+
{...props}
150+
/>
151+
))
152+
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
153+
154+
const ContextMenuSeparator = React.forwardRef<
155+
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
156+
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
157+
>(({ className, ...props }, ref) => (
158+
<ContextMenuPrimitive.Separator
159+
ref={ref}
160+
className={cn('-mx-1 my-1 h-px bg-border', className)}
161+
{...props}
162+
/>
163+
))
164+
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
165+
166+
function ContextMenuShortcut({
167+
className,
168+
...props
169+
}: React.HTMLAttributes<HTMLSpanElement>) {
170+
return (
171+
<span
172+
className={cn(
173+
'ml-auto text-xs tracking-widest text-muted-foreground',
174+
className,
175+
)}
176+
{...props}
177+
/>
178+
)
179+
}
180+
ContextMenuShortcut.displayName = 'ContextMenuShortcut'
181+
182+
export {
183+
ContextMenu,
184+
ContextMenuCheckboxItem,
185+
ContextMenuContent,
186+
ContextMenuGroup,
187+
ContextMenuItem,
188+
ContextMenuLabel,
189+
ContextMenuPortal,
190+
ContextMenuRadioGroup,
191+
ContextMenuRadioItem,
192+
ContextMenuSeparator,
193+
ContextMenuShortcut,
194+
ContextMenuSub,
195+
ContextMenuSubContent,
196+
ContextMenuSubTrigger,
197+
ContextMenuTrigger,
198+
}

packages/ui/src/index.tsx

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,52 @@
1+
import {
2+
ContextMenu,
3+
ContextMenuContent,
4+
ContextMenuItem,
5+
ContextMenuSeparator,
6+
ContextMenuTrigger,
7+
} from '@/components/ui/context-menu'
18
import CLIInterface from '@/page/index'
29
import { render } from 'preact'
310
import { Footer } from './components/footer'
411
import { Header } from './components/header'
12+
import { USER_EVENT } from './constants/USER_EVENT'
513
import './style.css'
614

7-
// document.addEventListener('contextmenu', (event) => {
8-
// event.preventDefault()
9-
// })
10-
1115
export function App() {
1216
return (
13-
<div class="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 h-[100vh] overflow-y-auto transition-colors duration-200 flex flex-col">
14-
<Header />
15-
<CLIInterface />
16-
<Footer />
17-
</div>
17+
<ContextMenu>
18+
<ContextMenuTrigger>
19+
<div class="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 h-[100vh] overflow-y-auto transition-colors duration-200 flex flex-col">
20+
<Header />
21+
<CLIInterface />
22+
<Footer />
23+
</div>
24+
</ContextMenuTrigger>
25+
<ContextMenuContent className="w-40">
26+
<ContextMenuItem
27+
onClick={() => window.ipc.postMessage(`${USER_EVENT.OPEN}|https://github.com/uni-helper/create-uni`)}
28+
>
29+
Github
30+
</ContextMenuItem>
31+
<ContextMenuItem
32+
onClick={() => window.ipc.postMessage(`${USER_EVENT.OPEN}|https://afdian.com/a/flippedround`)}
33+
>
34+
Sponsor
35+
</ContextMenuItem>
36+
<ContextMenuSeparator />
37+
<ContextMenuItem
38+
onClick={() => window.location.reload()}
39+
>
40+
Reload
41+
</ContextMenuItem>
42+
<ContextMenuItem
43+
onClick={() => window.ipc.postMessage(USER_EVENT.CLOSE)}
44+
>
45+
Exit
46+
</ContextMenuItem>
47+
</ContextMenuContent>
48+
</ContextMenu>
49+
1850
)
1951
}
2052

0 commit comments

Comments
 (0)