feat: app button support drag
This commit is contained in:
parent
f7cdf212bc
commit
4eba638a36
18 changed files with 277 additions and 125 deletions
|
@ -1,4 +1,4 @@
|
|||
// import type { Linter } from 'eslint'
|
||||
import type { Linter } from 'eslint'
|
||||
import globals from 'globals'
|
||||
import pluginJs from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
@ -34,7 +34,8 @@ export default [
|
|||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@eslint-react/no-array-index-key': 'off',
|
||||
'@eslint-react/hooks-extra/no-redundant-custom-hook': 'off',
|
||||
'@eslint-react/dom/no-missing-button-type': 'off'
|
||||
'@eslint-react/dom/no-missing-button-type': 'off',
|
||||
'@eslint-react/hooks-extra/prefer-use-state-lazy-initialization': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -101,8 +101,8 @@
|
|||
"@semantic-release/exec": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
|
|
|
@ -2,7 +2,7 @@ import Header from '@/app/content/views/Header'
|
|||
import Footer from '@/app/content/views/Footer'
|
||||
import Main from '@/app/content/views/Main'
|
||||
import AppButton from '@/app/content/views/AppButton'
|
||||
import AppContainer from '@/app/content/views/AppContainer'
|
||||
import AppMain from '@/app/content/views/AppMain'
|
||||
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||
import RoomDomain from '@/domain/Room'
|
||||
import UserInfoDomain from '@/domain/UserInfo'
|
||||
|
@ -10,6 +10,7 @@ import Setup from '@/app/content/views/Setup'
|
|||
import MessageListDomain from '@/domain/MessageList'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Toaster } from 'sonner'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
|
||||
import DanmakuContainer from './components/DanmakuContainer'
|
||||
import DanmakuDomain from '@/domain/Danmaku'
|
||||
|
@ -31,8 +32,8 @@ export default function App() {
|
|||
const danmakuDomain = useRemeshDomain(DanmakuDomain())
|
||||
const danmakuIsEnabled = useRemeshQuery(danmakuDomain.query.IsEnabledQuery())
|
||||
const userInfoSetFinished = useRemeshQuery(userInfoDomain.query.UserInfoSetIsFinishedQuery())
|
||||
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
||||
const messageListLoadFinished = useRemeshQuery(messageListDomain.query.LoadIsFinishedQuery())
|
||||
const userInfoLoadFinished = useRemeshQuery(userInfoDomain.query.UserInfoLoadIsFinishedQuery())
|
||||
|
||||
const notUserInfo = userInfoLoadFinished && !userInfoSetFinished
|
||||
|
||||
|
@ -58,14 +59,21 @@ export default function App() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AppContainer>
|
||||
<AppMain>
|
||||
<Header />
|
||||
<Main />
|
||||
<Footer />
|
||||
{notUserInfo && <Setup />}
|
||||
<AnimatePresence>
|
||||
{notUserInfo && (
|
||||
<motion.div initial={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.3 }}>
|
||||
<Setup></Setup>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Toaster richColors offset="70px" visibleToasts={1} position="top-center"></Toaster>
|
||||
</AppContainer>
|
||||
</AppMain>
|
||||
<AppButton></AppButton>
|
||||
|
||||
<DanmakuContainer ref={danmakuContainerRef} />
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { type FC } from 'react'
|
||||
import { FrownIcon, ThumbsUpIcon } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import LikeButton from './LikeButton'
|
||||
import FormatDate from './FormatDate'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/Avatar'
|
||||
|
|
|
@ -14,6 +14,7 @@ const MessageList: FC<MessageListProps> = ({ children }) => {
|
|||
return (
|
||||
<ScrollArea ref={setScrollParentRef}>
|
||||
<Virtuoso
|
||||
defaultItemHeight={108}
|
||||
followOutput={(isAtBottom: boolean) => (isAtBottom ? 'smooth' : 'auto')}
|
||||
initialTopMostItemIndex={{ index: 'LAST', align: 'end' }}
|
||||
data={children}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { type FC, useState, type MouseEvent, useRef } from 'react'
|
||||
import { SettingsIcon, MoonIcon, SunIcon } from 'lucide-react'
|
||||
import { type FC, useState, type MouseEvent, useRef, useEffect, useLayoutEffect } from 'react'
|
||||
import { SettingsIcon, MoonIcon, SunIcon, HandIcon } from 'lucide-react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
|
||||
import { browser } from 'wxt/browser'
|
||||
import { useRemeshDomain, useRemeshQuery, useRemeshSend } from 'remesh-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { EVENT } from '@/constants/event'
|
||||
|
@ -20,6 +19,8 @@ import LogoIcon6 from '@/assets/images/logo-6.svg'
|
|||
import AppStatusDomain from '@/domain/AppStatus'
|
||||
import { getDay } from 'date-fns'
|
||||
import { messenger } from '@/messenger'
|
||||
import useDarg from '@/hooks/useDarg'
|
||||
import { useWindowSize } from 'react-use'
|
||||
|
||||
const AppButton: FC = () => {
|
||||
const send = useRemeshSend()
|
||||
|
@ -29,6 +30,9 @@ const AppButton: FC = () => {
|
|||
const userInfoDomain = useRemeshDomain(UserInfoDomain())
|
||||
const userInfo = useRemeshQuery(userInfoDomain.query.UserInfoQuery())
|
||||
const toastDomain = useRemeshDomain(ToastDomain())
|
||||
const appPosition = useRemeshQuery(appStatusDomain.query.PositionQuery())
|
||||
const appStatusLoadIsFinished = useRemeshQuery(appStatusDomain.query.StatusLoadIsFinishedQuery())
|
||||
|
||||
const DayLogo = [LogoIcon0, LogoIcon1, LogoIcon2, LogoIcon3, LogoIcon4, LogoIcon5, LogoIcon6][getDay(Date())]
|
||||
|
||||
const isDarkMode =
|
||||
|
@ -38,6 +42,21 @@ const AppButton: FC = () => {
|
|||
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
|
||||
const { x, y, ref } = useDarg({
|
||||
initX: appPosition.x,
|
||||
initY: appPosition.y,
|
||||
minX: 44,
|
||||
maxX: width - 44,
|
||||
maxY: height - 22,
|
||||
minY: height / 2
|
||||
})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
appStatusLoadIsFinished && send(appStatusDomain.command.UpdatePositionCommand({ x, y }))
|
||||
}, [x, y])
|
||||
|
||||
useClickAway(menuRef, () => {
|
||||
setMenuOpen(false)
|
||||
}, ['click'])
|
||||
|
@ -65,7 +84,15 @@ const AppButton: FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref={menuRef} className="fixed bottom-5 right-5 z-infinity grid select-none justify-center gap-y-3">
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed bottom-5 right-5 z-infinity grid w-min select-none justify-center gap-y-3"
|
||||
style={{
|
||||
left: `calc(${appPosition.x}px)`,
|
||||
bottom: `calc(100vh - ${appPosition.y}px)`,
|
||||
transform: 'translateX(-50%)'
|
||||
}}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{menuOpen && (
|
||||
<motion.div
|
||||
|
@ -92,13 +119,12 @@ const AppButton: FC = () => {
|
|||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleOpenOptionsPage}
|
||||
variant="outline"
|
||||
className="pointer-events-auto size-10 rounded-full p-0 shadow"
|
||||
>
|
||||
<Button onClick={handleOpenOptionsPage} variant="outline" className="size-10 rounded-full p-0 shadow">
|
||||
<SettingsIcon size={20} />
|
||||
</Button>
|
||||
<Button ref={ref} variant="outline" className="size-10 cursor-grab rounded-full p-0 shadow">
|
||||
<HandIcon size={20} />
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { type ReactNode, type FC } from 'react'
|
||||
import useResizable from '@/hooks/useResizable'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import AppStatusDomain from '@/domain/AppStatus'
|
||||
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
||||
|
||||
export interface AppContainerProps {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const AppContainer: FC<AppContainerProps> = ({ children }) => {
|
||||
const appStatusDomain = useRemeshDomain(AppStatusDomain())
|
||||
const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery())
|
||||
|
||||
const { size, ref } = useResizable({
|
||||
initSize: Math.max(375, window.innerWidth / 6),
|
||||
maxSize: Math.min(750, window.innerWidth / 3),
|
||||
minSize: Math.max(375, window.innerWidth / 6),
|
||||
direction: 'left'
|
||||
})
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{appOpenStatus && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, x: 10 }}
|
||||
animate={{ opacity: 1, y: 0, x: 0 }}
|
||||
exit={{ opacity: 0, y: 10, x: 10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="fixed bottom-10 right-10 z-infinity box-border grid h-screen max-h-[min(calc(100vh_-60px),_1200px)] grid-flow-col grid-rows-[auto_1fr_auto] rounded-xl bg-slate-50 font-sans shadow-2xl"
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
ref={ref}
|
||||
className="absolute inset-y-3 -left-0.5 z-20 w-1 cursor-ew-resize rounded-xl bg-slate-100 opacity-0 shadow transition-opacity duration-200 ease-in hover:opacity-100"
|
||||
></div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
AppContainer.displayName = 'AppContainer'
|
||||
|
||||
export default AppContainer
|
67
src/app/content/views/AppMain/index.tsx
Normal file
67
src/app/content/views/AppMain/index.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { type ReactNode, type FC, useState } from 'react'
|
||||
import useResizable from '@/hooks/useResizable'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import AppStatusDomain from '@/domain/AppStatus'
|
||||
import { useRemeshDomain, useRemeshQuery } from 'remesh-react'
|
||||
import { cn } from '@/utils'
|
||||
import { useWindowSize } from 'react-use'
|
||||
|
||||
export interface AppMainProps {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const AppMain: FC<AppMainProps> = ({ children }) => {
|
||||
const appStatusDomain = useRemeshDomain(AppStatusDomain())
|
||||
const appOpenStatus = useRemeshQuery(appStatusDomain.query.OpenQuery())
|
||||
const { x, y } = useRemeshQuery(appStatusDomain.query.PositionQuery())
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const isOnRightSide = x >= width / 2 + 44
|
||||
|
||||
const { size, ref } = useResizable({
|
||||
initSize: Math.max(375, width / 6),
|
||||
maxSize: Math.max(Math.min(750, width / 3), 375),
|
||||
minSize: Math.max(375, width / 6),
|
||||
direction: isOnRightSide ? 'left' : 'right'
|
||||
})
|
||||
|
||||
const [isAnimationComplete, setAnimationComplete] = useState(false)
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{appOpenStatus && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, x: isOnRightSide ? '-100%' : '0' }}
|
||||
animate={{ opacity: 1, y: 0, x: isOnRightSide ? '-100%' : '0' }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.3, ease: 'linear' }}
|
||||
onAnimationEnd={() => setAnimationComplete(true)}
|
||||
onAnimationStart={() => setAnimationComplete(false)}
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
left: `${x}px`,
|
||||
bottom: `calc(100vh - ${y}px + 22px)`
|
||||
}}
|
||||
className={cn(
|
||||
'fixed inset-y-10 right-10 z-infinity mb-0 mt-auto box-border grid max-h-[min(calc(100vh_-60px),_1000px)] grid-flow-col grid-rows-[auto_1fr_auto] rounded-xl bg-slate-50 font-sans shadow-2xl',
|
||||
{ 'transition-transform': isAnimationComplete }
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'absolute inset-y-3 z-20 w-1 cursor-ew-resize rounded-xl bg-slate-100 opacity-0 shadow transition-opacity duration-200 ease-in hover:opacity-100',
|
||||
isOnRightSide ? '-left-0.5' : '-right-0.5'
|
||||
)}
|
||||
></div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
AppMain.displayName = 'AppMain'
|
||||
|
||||
export default AppMain
|
|
@ -7,7 +7,6 @@ import PromptItem from '../../components/PromptItem'
|
|||
import UserInfoDomain from '@/domain/UserInfo'
|
||||
import RoomDomain, { MessageType } from '@/domain/Room'
|
||||
import MessageListDomain from '@/domain/MessageList'
|
||||
import BlurFade from '@/components/magicui/BlurFade'
|
||||
|
||||
const Main: FC = () => {
|
||||
const send = useRemeshSend()
|
||||
|
@ -39,7 +38,6 @@ const Main: FC = () => {
|
|||
<MessageList>
|
||||
{messageList.map((message, index) =>
|
||||
message.type === MessageType.Normal ? (
|
||||
<BlurFade key={message.id} duration={0.1} yOffset={0}>
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
data={message}
|
||||
|
@ -47,16 +45,14 @@ const Main: FC = () => {
|
|||
hate={message.hate}
|
||||
onLikeChange={() => handleLikeChange(message.id)}
|
||||
onHateChange={() => handleHateChange(message.id)}
|
||||
className="duration-300 animate-in fade-in-0"
|
||||
></MessageItem>
|
||||
</BlurFade>
|
||||
) : (
|
||||
<BlurFade key={message.id} duration={0.1} yOffset={0}>
|
||||
<PromptItem
|
||||
key={message.id}
|
||||
data={message}
|
||||
className={`${index === 0 ? 'pt-4' : ''} ${index === messageList.length - 1 ? 'pb-4' : ''}`}
|
||||
></PromptItem>
|
||||
</BlurFade>
|
||||
)
|
||||
)}
|
||||
</MessageList>
|
||||
|
|
|
@ -68,6 +68,7 @@ const Setup: FC = () => {
|
|||
const messageListDomain = useRemeshDomain(MessageListDomain())
|
||||
|
||||
const [userInfo, setUserInfo] = useState<UserInfo>()
|
||||
|
||||
const handleSetup = () => {
|
||||
send(userInfoDomain.command.UpdateUserInfoCommand(userInfo!))
|
||||
send(messageListDomain.command.ClearListCommand())
|
||||
|
|
|
@ -67,7 +67,7 @@ const Markdown: FC<MarkdownProps> = ({ children = '', className }) => {
|
|||
input: ({ className, ...props }) => <input className={cn('my-0', className)} {...props} />,
|
||||
table: ({ className, ...props }) => (
|
||||
<div className="my-2 w-full">
|
||||
<ScrollArea>
|
||||
<ScrollArea scrollLock={false}>
|
||||
<table className={cn('my-0 w-full rounded-md', className)} {...props} />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
|
@ -106,7 +106,7 @@ const Markdown: FC<MarkdownProps> = ({ children = '', className }) => {
|
|||
*
|
||||
*/
|
||||
code: ({ className, ...props }) => (
|
||||
<ScrollArea>
|
||||
<ScrollArea className="overscroll-y-auto" scrollLock={false}>
|
||||
<code className={cn('text-sm', className)} {...props}></code>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
|
|
|
@ -5,10 +5,13 @@ import { cn } from '@/utils/index'
|
|||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & { scrollLock?: boolean }
|
||||
>(({ className, children, scrollLock = true, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root className={cn('relative grid grid-rows-[1fr] overflow-hidden', className)} {...props}>
|
||||
<ScrollAreaPrimitive.Viewport ref={ref} className="size-full overscroll-none rounded-[inherit]">
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
ref={ref}
|
||||
className={cn('size-full rounded-[inherit]', scrollLock ? 'overscroll-none' : 'overscroll-auto')}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
|
|
|
@ -9,11 +9,13 @@ import { map } from 'rxjs'
|
|||
export interface AppStatus {
|
||||
open: boolean
|
||||
unread: number
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
export const defaultStatusState = {
|
||||
open: false,
|
||||
unread: 0
|
||||
unread: 0,
|
||||
position: { x: window.innerWidth - 44, y: window.innerHeight - 22 }
|
||||
}
|
||||
|
||||
const AppStatusDomain = Remesh.domain({
|
||||
|
@ -32,8 +34,8 @@ const AppStatusDomain = Remesh.domain({
|
|||
|
||||
const StatusLoadIsFinishedQuery = domain.query({
|
||||
name: 'AppStatus.StatusLoadIsFinishedQuery',
|
||||
impl: () => {
|
||||
return StatusLoadModule.query.IsFinishedQuery()
|
||||
impl: ({ get }) => {
|
||||
return get(StatusLoadModule.query.IsFinishedQuery())
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -56,6 +58,13 @@ const AppStatusDomain = Remesh.domain({
|
|||
}
|
||||
})
|
||||
|
||||
const PositionQuery = domain.query({
|
||||
name: 'AppStatus.PositionQuery',
|
||||
impl: ({ get }) => {
|
||||
return get(StatusState()).position
|
||||
}
|
||||
})
|
||||
|
||||
const HasUnreadQuery = domain.query({
|
||||
name: 'AppStatus.HasUnreadQuery',
|
||||
impl: ({ get }) => {
|
||||
|
@ -68,6 +77,7 @@ const AppStatusDomain = Remesh.domain({
|
|||
impl: ({ get }, value: boolean) => {
|
||||
const status = get(StatusState())
|
||||
return UpdateStatusCommand({
|
||||
...status,
|
||||
unread: value ? 0 : status.unread,
|
||||
open: value
|
||||
})
|
||||
|
@ -85,6 +95,17 @@ const AppStatusDomain = Remesh.domain({
|
|||
}
|
||||
})
|
||||
|
||||
const UpdatePositionCommand = domain.command({
|
||||
name: 'AppStatus.UpdatePositionCommand',
|
||||
impl: ({ get }, value: { x: number; y: number }) => {
|
||||
const status = get(StatusState())
|
||||
return UpdateStatusCommand({
|
||||
...status,
|
||||
position: value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const UpdateStatusCommand = domain.command({
|
||||
name: 'AppStatus.UpdateStatusCommand',
|
||||
impl: (_, value: AppStatus) => {
|
||||
|
@ -128,11 +149,13 @@ const AppStatusDomain = Remesh.domain({
|
|||
OpenQuery,
|
||||
UnreadQuery,
|
||||
HasUnreadQuery,
|
||||
PositionQuery,
|
||||
StatusLoadIsFinishedQuery
|
||||
},
|
||||
command: {
|
||||
UpdateOpenCommand,
|
||||
UpdateUnreadCommand
|
||||
UpdateUnreadCommand,
|
||||
UpdatePositionCommand
|
||||
},
|
||||
event: {
|
||||
SyncToStorageEvent
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Remesh } from 'remesh'
|
|||
import { DanmakuExtern } from './externs/Danmaku'
|
||||
import RoomDomain, { TextMessage } from './Room'
|
||||
import UserInfoDomain from './UserInfo'
|
||||
import { map, merge, of } from 'rxjs'
|
||||
import { map, merge } from 'rxjs'
|
||||
|
||||
const DanmakuDomain = Remesh.domain({
|
||||
name: 'DanmakuDomain',
|
||||
|
|
|
@ -4,7 +4,7 @@ import localStorageDriver from 'unstorage/drivers/localstorage'
|
|||
import { LocalStorageExtern, IndexDBStorageExtern, BrowserSyncStorageExtern } from '@/domain/externs/Storage'
|
||||
import { STORAGE_NAME } from '@/constants/config'
|
||||
import { webExtensionDriver } from '@/utils/webExtensionDriver'
|
||||
import { browser } from 'wxt/browser'
|
||||
|
||||
import { Storage } from '@/domain/externs/Storage'
|
||||
import { EVENT } from '@/constants/event'
|
||||
|
||||
|
@ -62,23 +62,3 @@ export const BrowserSyncStorageImpl = BrowserSyncStorageExtern.impl({
|
|||
watch: browserSyncStorage.watch as Storage['watch'],
|
||||
unwatch: browserSyncStorage.unwatch
|
||||
})
|
||||
|
||||
// export const BrowserSyncStorageImpl = BrowserSyncStorageExtern.impl({
|
||||
// name: STORAGE_NAME,
|
||||
// get: async (key: string) => {
|
||||
// const res = await browser.storage.sync.get(key)
|
||||
// return res[key] ?? null
|
||||
// },
|
||||
// set: async (key, value) => {
|
||||
// await browser.storage.sync.set({ [key]: value ?? null })
|
||||
// },
|
||||
// remove: browserSyncStorage.removeItem,
|
||||
// clear: browserSyncStorage.clear,
|
||||
// watch: async (callback) => {
|
||||
// browser.storage.sync.onChanged.addListener(callback)
|
||||
// return async () => {
|
||||
// return browser.storage.sync.onChanged.removeListener(callback)
|
||||
// }
|
||||
// },
|
||||
// unwatch: browserSyncStorage.unwatch
|
||||
// })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type RemeshEvent, type RemeshAction, type RemeshDomainContext, type RemeshExtern } from 'remesh'
|
||||
import { defer, from, map, Observable, switchMap } from 'rxjs'
|
||||
import { from, map, Observable, switchMap } from 'rxjs'
|
||||
|
||||
import { Storage, StorageValue } from '@/domain/externs/Storage'
|
||||
|
||||
|
|
91
src/hooks/useDarg.ts
Normal file
91
src/hooks/useDarg.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import { clamp, isInRange } from '@/utils'
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||
|
||||
export interface DargOptions {
|
||||
initX: number
|
||||
initY: number
|
||||
maxX: number
|
||||
minX: number
|
||||
maxY: number
|
||||
minY: number
|
||||
}
|
||||
|
||||
const useDarg = (options: DargOptions) => {
|
||||
const { initX, initY, maxX = 0, minX = 0, maxY = 0, minY = 0 } = options
|
||||
|
||||
const mousePosition = useRef({ x: 0, y: 0 })
|
||||
|
||||
const [position, setPosition] = useState({ x: clamp(initX, minX, maxX), y: clamp(initY, minY, maxY) })
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setPosition({ x: clamp(initX, minX, maxX), y: clamp(initY, minY, maxY) })
|
||||
}, [initX, initY, maxX, minX, maxY, minY])
|
||||
|
||||
const isMove = useRef(false)
|
||||
|
||||
const handleMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (isMove.current) {
|
||||
const { clientX, clientY } = e
|
||||
const delta = {
|
||||
x: position.x + clientX - mousePosition.current.x,
|
||||
y: position.y + clientY - mousePosition.current.y
|
||||
}
|
||||
|
||||
const hasChanged = delta.x !== position.x || delta.y !== position.y
|
||||
|
||||
if (isInRange(delta.x, minX, maxX)) {
|
||||
mousePosition.current.x = clientX
|
||||
}
|
||||
if (isInRange(delta.y, minY, maxY)) {
|
||||
mousePosition.current.y = clientY
|
||||
}
|
||||
if (hasChanged) {
|
||||
setPosition(() => {
|
||||
const x = clamp(delta.x, minX, maxX)
|
||||
const y = clamp(delta.y, minY, maxY)
|
||||
return { x, y }
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[minX, maxX, minY, maxY, position]
|
||||
)
|
||||
|
||||
const handleEnd = useCallback(() => {
|
||||
isMove.current = false
|
||||
document.documentElement.style.cursor = ''
|
||||
document.documentElement.style.userSelect = ''
|
||||
}, [])
|
||||
|
||||
const handleStart = useCallback((e: MouseEvent) => {
|
||||
const { clientX, clientY } = e
|
||||
mousePosition.current = { x: clientX, y: clientY }
|
||||
isMove.current = true
|
||||
document.documentElement.style.userSelect = 'none'
|
||||
document.documentElement.style.cursor = 'grab'
|
||||
}, [])
|
||||
|
||||
const handleRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
const setHandleRef = useCallback(
|
||||
(node: HTMLElement | null) => {
|
||||
if (handleRef.current) {
|
||||
handleRef.current.removeEventListener('mousedown', handleStart)
|
||||
document.removeEventListener('mouseup', handleEnd)
|
||||
document.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
if (node) {
|
||||
node.addEventListener('mousedown', handleStart)
|
||||
document.addEventListener('mouseup', handleEnd)
|
||||
document.addEventListener('mousemove', handleMove)
|
||||
}
|
||||
handleRef.current = node
|
||||
},
|
||||
[handleEnd, handleMove, handleStart]
|
||||
)
|
||||
|
||||
return { ref: setHandleRef, ...position }
|
||||
}
|
||||
|
||||
export default useDarg
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { clamp, isInRange } from '@/utils'
|
||||
|
||||
export interface ResizableOptions {
|
||||
|
@ -11,7 +11,11 @@ export interface ResizableOptions {
|
|||
const useResizable = (options: ResizableOptions) => {
|
||||
const { minSize, maxSize, initSize = 0, direction } = options
|
||||
|
||||
const [size, setSize] = useState(initSize)
|
||||
const [size, setSize] = useState(clamp(initSize, minSize, maxSize))
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setSize(clamp(initSize, minSize, maxSize))
|
||||
}, [initSize, minSize, maxSize])
|
||||
|
||||
const position = useRef(0)
|
||||
|
||||
|
@ -67,13 +71,13 @@ const useResizable = (options: ResizableOptions) => {
|
|||
[isHorizontal]
|
||||
)
|
||||
|
||||
const ref = useRef<HTMLElement | null>(null)
|
||||
const handlerRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
// Watch ref: https://medium.com/@teh_builder/ref-objects-inside-useeffect-hooks-eb7c15198780
|
||||
const setRef = useCallback(
|
||||
const setHandleRef = useCallback(
|
||||
(node: HTMLElement | null) => {
|
||||
if (ref.current) {
|
||||
ref.current.removeEventListener('mousedown', handleStart)
|
||||
if (handlerRef.current) {
|
||||
handlerRef.current.removeEventListener('mousedown', handleStart)
|
||||
document.removeEventListener('mouseup', handleEnd)
|
||||
document.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
@ -82,12 +86,12 @@ const useResizable = (options: ResizableOptions) => {
|
|||
document.addEventListener('mouseup', handleEnd)
|
||||
document.addEventListener('mousemove', handleMove)
|
||||
}
|
||||
ref.current = node
|
||||
handlerRef.current = node
|
||||
},
|
||||
[handleEnd, handleMove, handleStart]
|
||||
)
|
||||
|
||||
return { size, ref: setRef }
|
||||
return { size, ref: setHandleRef }
|
||||
}
|
||||
|
||||
export default useResizable
|
||||
|
|
Loading…
Reference in a new issue