Skip to content

Commit

Permalink
feat: 添加App Store、Launchpad
Browse files Browse the repository at this point in the history
  • Loading branch information
draco-china committed Jul 18, 2023
1 parent 4830687 commit 3cf01dc
Show file tree
Hide file tree
Showing 15 changed files with 616 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/icons/exit-full.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/icons/full.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions src/lib/getFavicon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export default function getFaviconUrl(url: string): Promise<string> {
return new Promise((resolve, reject) => {
// 尝试从固定URL中获取favicon
const fixedUrl = `${url}/favicon.ico`;
const fixedImg = new Image();
fixedImg.src = fixedUrl;
fixedImg.onload = function () {
resolve(fixedUrl);
};
fixedImg.onerror = function () {
// 如果固定URL中没有找到favicon,则从HTML文档中查找
fetch(url)
.then((response) => response.text())
.then((html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const linkTags = doc.querySelectorAll(
'link[rel="icon"], link[rel="shortcut icon"]',
);
let faviconUrl: string | null = null;
if (linkTags.length > 0) {
faviconUrl = linkTags[0].getAttribute('href');
if (faviconUrl && faviconUrl.startsWith('/')) {
faviconUrl = `${url}${faviconUrl}`;
}
}
if (faviconUrl) {
resolve(faviconUrl);
} else {
reject('找不到网站favicon');
}
})
.catch((error) => {
reject(`获取HTML文档失败:${error}`);
});
};
});
}
2 changes: 1 addition & 1 deletion src/pages/mac/components/action-center.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default function ActionCenter() {
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" className="h-full">
<Icon icon="action-center" />
<Icon icon="switch" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-max bg-opacity-40 ">
Expand Down
169 changes: 169 additions & 0 deletions src/pages/mac/components/app-store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Input,
Label,
RadioGroup,
RadioGroupItem,
} from '@/components';
import getFaviconUrl from '@/lib/getFavicon';
import { useApps, useWindow } from '@/store';
import { useState } from 'react';

function AppStore() {
const { actions } = useApps();
const window = useWindow('app-store');
const [state, setState] = useState({
type: 'url',
name: '',
address: '',
icon: '',
});
return (
<Card className="border-0">
<CardHeader>
<CardTitle>添加应用</CardTitle>
<CardDescription>添加一个新的应用到应用列表</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-around space-x-6">
<div className="flex flex-col items-center space-y-6">
<Avatar className="h-20 w-20">
<AvatarImage src={state.icon} alt={state.name} />
<AvatarFallback>{state.name}</AvatarFallback>
</Avatar>
<Button size="sm">
<Input
id="file"
type="file"
className="hidden"
accept="image/*"
max={1}
onChange={(e) => {
let files = e.target.files;
let file = files?.[0];

if (file) {
let reader = new FileReader();
reader.onload = (e) => {
let data = e.target?.result as string;
// data即为图片的base64编码
setState((prev) => ({ ...prev, icon: data }));
};

reader.readAsDataURL(file);
}
}}
/>
<Label htmlFor="file">上传图片</Label>
</Button>
</div>
<div className="flex-1 space-y-6">
<Input
placeholder="应用名称"
value={state.name}
onChange={(e) => {
setState((prev) => ({ ...prev, name: e.target.value }));
}}
/>
<Input
placeholder="应用地址"
value={state.address}
onChange={(e) => {
setState((prev) => ({ ...prev, address: e.target.value }));
if (state.icon) return;
getFaviconUrl(e.target.value).then((icon) => {
setState((prev) => ({ ...prev, icon }));
});
}}
/>
<RadioGroup
id="type"
className="flex items-center"
value={state.type}
onValueChange={(value) => {
setState((prev) => ({ ...prev, type: value }));
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="url" id="url" />
<Label htmlFor="url">外部窗口</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="iframe" id="iframe" />
<Label htmlFor="iframe">应用窗口</Label>
</div>
</RadioGroup>
</div>
</div>
</CardContent>
<CardFooter className="absolute bottom-0 left-0 flex w-full items-center justify-center space-x-6">
<Button
onClick={(e) => {
e.stopPropagation();
window.actions.close();
}}
>
取消
</Button>
<Button
onClick={() => {
actions.add({
id: Date.now().toString(),
name: state.name,
icon: state.icon,
url: state.type === 'url' ? state.address : undefined,
iframe: state.type === 'iframe' ? state.address : undefined,
});
setState({
type: 'url',
name: '',
address: '',
icon: '',
});
}}
>
添加
</Button>
</CardFooter>
</Card>

// <Form>
// <FormItem lable="应用名称" name="name">
// <Input />
// </FormItem>
// <FormItem lable="应用图标" name="icon">
// <Input />
// </FormItem>
// <FormItem lable="应用地址" name="url">
// <Input />
// </FormItem>
// <FormItem lable="窗口模式" name="url">
// <RadioGroup defaultValue="url">
// <div className="flex items-center space-x-2">
// <RadioGroupItem value="url" id="url" />
// <Label htmlFor="url">新窗口</Label>
// </div>
// <div className="flex items-center space-x-2">
// <RadioGroupItem value="iframe" id="iframe" />
// <Label htmlFor="iframe">集成窗口</Label>
// </div>
// </RadioGroup>
// <FormDescription>
// 新窗口
// 为打开新标签页;集成窗口为桌面窗口模式,此模式可能某些网站无法打开
// </FormDescription>
// </FormItem>
// </Form>
);
}

export default AppStore;
8 changes: 7 additions & 1 deletion src/pages/mac/components/dock-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ export default function DockItem({ id, mouseX, size }: ItemProps) {
return (
<li
className="group flex flex-col items-center justify-center"
onClick={actions.open}
onClick={() => {
if (app.url) {
window.open(app.url, '_blank');
} else {
actions.open();
}
}}
>
<motion.span
className="relative"
Expand Down
9 changes: 6 additions & 3 deletions src/pages/mac/components/dock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Separator } from '@/components';
import { useDock } from '@/store';
import { useDock, useWindow } from '@/store';
import { useMotionValue } from 'framer-motion';
import { ReactSortable } from 'react-sortablejs';
import DockItem from './dock-item';
Expand All @@ -10,6 +10,7 @@ interface DockProps {

export default function Dock({ size }: DockProps) {
const { apps, actions } = useDock();
const { open } = useWindow('launchpad');
const mouseX = useMotionValue<number | null>(null);

const handleMouseMove = (event: React.MouseEvent<any, MouseEvent>) => {
Expand All @@ -21,7 +22,9 @@ export default function Dock({ size }: DockProps) {

return (
<footer
className="fixed bottom-0 left-1/2 mb-2 flex -translate-x-1/2 justify-center space-x-2 rounded-3xl bg-background/60 px-3 py-1 shadow-md"
className={`fixed bottom-0 left-1/2 mb-2 flex -translate-x-1/2 justify-center space-x-2 rounded-3xl bg-background/60 px-3 py-1 shadow-md ${
open ? 'z-20' : ``
}`}
style={{
height: `${size}rem`,
}}
Expand Down Expand Up @@ -99,7 +102,7 @@ export default function Dock({ size }: DockProps) {
tag="ul"
className="flex h-full w-max items-end justify-start space-x-2"
>
{['github'].map((app) => (
{['vscode', 'github'].map((app) => (
<DockItem key={app} id={app} mouseX={mouseX} size={size} />
))}
</ReactSortable>
Expand Down
104 changes: 104 additions & 0 deletions src/pages/mac/components/launchpad.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useApp, useLaunchpad, useWindow } from '@/store';
import { motion } from 'framer-motion';
import { Search } from 'lucide-react';
import { useState } from 'react';
import { ReactSortable } from 'react-sortablejs';

interface LaunchpadProps {
size: number;
background: string;
}

function Item({ id }: { id: string }) {
const { app } = useApp(id);
const { actions } = useWindow(id);

return (
<li className="flex justify-center active:opacity-60">
<div className="flex flex-col items-center justify-center">
<img
className="W-16 h-16 cursor-pointer rounded-2xl bg-foreground"
draggable={false}
src={app?.icon}
onClick={() => {
if (app.url) {
window.open(app.url, '_blank');
} else {
actions.open();
}
}}
/>
<span className="w-max whitespace-normal pt-2 text-base text-foreground">
{app.name}
</span>
</div>
</li>
);
}

function Launchpad({ size, background }: LaunchpadProps) {
const { apps, actions } = useLaunchpad();
const { open } = useWindow('launchpad');

const [query, setQuery] = useState('');
if (!open) return null;
return (
<>
<div
className="fixed top-0 z-10 h-screen w-screen overflow-hidden"
style={{ backgroundImage: `url(${background})` }}
>
<img src={background} className="blur-2xl" />
</div>
<motion.section
className="fixed z-20 flex h-screen w-screen flex-col items-center"
style={{ height: `calc(100vh - ${size}rem)` }}
initial={{ opacity: 0, scale: 1.2 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.2 }}
transition={{
duration: 0.8,
type: 'spring',
}}
>
<div className="mb-12 mt-6 flex h-max w-max items-center justify-center rounded-lg border-foreground/30 bg-background/50 p-1 text-foreground">
<Search className="mx-2 h-4 w-4" />
<input
className=" h-6 w-48 flex-auto rounded-lg bg-transparent shadow-md outline-0 ring-0"
type="text"
placeholder="Search"
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<ReactSortable
group={{
name: 'launchpad',
pull: 'clone',
put: false,
}}
list={apps.map((id) => ({ id }))}
setList={(newState) => {
actions.set(newState.map((item) => item.id));
}}
ghostClass="invisible"
animation={150}
delay={2}
tag="ul"
className="grid w-full max-w-7xl grid-cols-7 gap-x-[10px] gap-y-[36px] p-[20px]"
>
{apps
.filter(
(item) =>
item.toLocaleUpperCase().indexOf(query.toLocaleUpperCase()) >
-1,
)
.map((id) => (
<Item key={id} id={id} />
))}
</ReactSortable>
</motion.section>
</>
);
}

export default Launchpad;
Loading

0 comments on commit 3cf01dc

Please sign in to comment.