-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4830687
commit 3cf01dc
Showing
15 changed files
with
616 additions
and
24 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
}); | ||
}; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.