diff --git a/src/main/main.ts b/src/main/main.ts index 96da219..0518753 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -29,6 +29,7 @@ const createWindow = () => { height: 800, minHeight: 800, title: '', + titleBarStyle: 'hidden', }); // and load the index.html of the app. @@ -126,4 +127,28 @@ app.whenReady().then(() => { ipcMain.handle('change-tray-icon', (event, icon: string) => { tray?.setImage(getTrayIcon(icon)); }); + ipcMain.handle('get-always-on-top', () => { + return mainWindow?.isAlwaysOnTop(); + }); + ipcMain.handle('set-always-on-top', (event, isAlwaysOnTop: boolean) => { + if (isAlwaysOnTop) { + mainWindow?.setAlwaysOnTop(true, 'screen-saver'); + } else { + mainWindow?.focus(); + mainWindow?.setAlwaysOnTop(false); + } + }); + ipcMain.handle('get-minimized', () => { + const [, height] = mainWindow?.getMinimumSize() || [0, 0]; + return height === 220; + }); + ipcMain.handle('set-minimized', (event, isMinimized: boolean) => { + if (isMinimized) { + mainWindow?.setMinimumSize(400, 220); + mainWindow?.setSize(400, 220); + } else { + mainWindow?.setMinimumSize(400, 800); + mainWindow?.setSize(400, 800); + } + }); }); diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 48ae724..8fead74 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -8,6 +8,11 @@ const electronAPI: IElectronAPI = { showWindow: () => ipcRenderer.send('show-window'), getMachineId: () => ipcRenderer.invoke('get-machine-id'), changeTrayIcon: (icon: string) => ipcRenderer.invoke('change-tray-icon', icon), + getAlwaysOnTop: () => ipcRenderer.invoke('get-always-on-top'), + setAlwaysOnTop: (isAlwaysOnTop: boolean) => + ipcRenderer.invoke('set-always-on-top', isAlwaysOnTop), + getMinimized: () => ipcRenderer.invoke('get-minimized'), + setMinimized: (isMinimized: boolean) => ipcRenderer.invoke('set-minimized', isMinimized), }; contextBridge.exposeInMainWorld('electronAPI', electronAPI); diff --git a/src/renderer/app/index.css b/src/renderer/app/index.css index ad1a825..ba0e385 100644 --- a/src/renderer/app/index.css +++ b/src/renderer/app/index.css @@ -100,3 +100,13 @@ @apply text-[12px] font-normal leading-[16px] tracking-[-.01em]; } } + +.drag-region { + -webkit-app-region: drag; +} +.drag-region > *:not(.drag-region) { + -webkit-app-region: no-drag; +} +.no-drag-region { + -webkit-app-region: no-drag; +} diff --git a/src/renderer/pages/pomodoro.tsx b/src/renderer/pages/pomodoro.tsx index 70ff98a..10b3162 100644 --- a/src/renderer/pages/pomodoro.tsx +++ b/src/renderer/pages/pomodoro.tsx @@ -8,8 +8,8 @@ import { TimeoutDialog } from '@/features/pomodoro/ui/timeout-dialog'; import { useFocusNotification } from '@/features/time'; import { useUser } from '@/features/user'; import { MINUTES_GAP } from '@/shared/constants'; -import { useDisclosure } from '@/shared/hooks'; -import { SidebarLayout, SimpleLayout, useToast } from '@/shared/ui'; +import { useAlwaysOnTop, useDisclosure, useMinimize } from '@/shared/hooks'; +import { useToast } from '@/shared/ui'; import { createIsoDuration, isoDurationToMs, @@ -54,6 +54,8 @@ const Pomodoro = () => { const { data: user } = useUser(); const { mutate: updateCategory } = useUpdateCategory(); const { mutate: savePomodoro } = useAddPomodoro(); + const { minimized, setMinimized } = useMinimize(); + const { alwaysOnTop, setAlwaysOnTop } = useAlwaysOnTop(); const [currentCategory, setCurrentCategory] = useState(categories?.[0]); const currentCategoryTitle = currentCategory?.title || ''; @@ -138,71 +140,77 @@ const Pomodoro = () => { setSelectedNextAction(undefined); }; + useEffect(() => { + // 휴식 대기중이거나 시작 대기전에는 최소화 및 항상 위에 표시를 해제 + if (mode !== 'focus' && mode !== 'rest') { + setMinimized(false); + setAlwaysOnTop(false); + } + }, [mode]); + if (mode === 'focus') return ( - - { - startRestWait(); - }} - handleEnd={() => { - endPomodoro(); - }} - /> - + ); if (mode === 'rest-wait') return ( - - { - updateCategoryTime('focusTime', currentFocusMinutes); - startRest(); - }} - handleEnd={() => { - updateCategoryTime('focusTime', currentFocusMinutes); - endPomodoro(); - }} - /> - + { + updateCategoryTime('focusTime', currentFocusMinutes); + startRest(); + }} + handleEnd={() => { + updateCategoryTime('focusTime', currentFocusMinutes); + endPomodoro(); + }} + /> ); if (mode === 'rest') return ( - - { - updateCategoryTime('restTime', currentRestMinutes); - startFocus(); - }} - handleEnd={() => { - updateCategoryTime('restTime', currentRestMinutes); - endPomodoro(); - }} - /> - + { + updateCategoryTime('restTime', currentRestMinutes); + startFocus(); + }} + handleEnd={() => { + updateCategoryTime('restTime', currentRestMinutes); + endPomodoro(); + }} + setMinimized={setMinimized} + setAlwaysOnTop={setAlwaysOnTop} + /> ); return ( - + <> { description={timeoutMessageMap[timeoutMode].description} /> )} - + ); }; diff --git a/src/renderer/shared/assets/images/hairball.png b/src/renderer/shared/assets/images/hairball.png new file mode 100644 index 0000000..7855c8c Binary files /dev/null and b/src/renderer/shared/assets/images/hairball.png differ diff --git a/src/renderer/shared/assets/svgs/minimize-off.svg b/src/renderer/shared/assets/svgs/minimize-off.svg new file mode 100644 index 0000000..8ab8a0d --- /dev/null +++ b/src/renderer/shared/assets/svgs/minimize-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/shared/assets/svgs/minimize-on.svg b/src/renderer/shared/assets/svgs/minimize-on.svg new file mode 100644 index 0000000..bdea5ed --- /dev/null +++ b/src/renderer/shared/assets/svgs/minimize-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/shared/assets/svgs/pin-off.svg b/src/renderer/shared/assets/svgs/pin-off.svg new file mode 100644 index 0000000..0a638ca --- /dev/null +++ b/src/renderer/shared/assets/svgs/pin-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/shared/assets/svgs/pin-on.svg b/src/renderer/shared/assets/svgs/pin-on.svg new file mode 100644 index 0000000..98c077f --- /dev/null +++ b/src/renderer/shared/assets/svgs/pin-on.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/shared/hooks/index.ts b/src/renderer/shared/hooks/index.ts index e492e3e..57780a0 100644 --- a/src/renderer/shared/hooks/index.ts +++ b/src/renderer/shared/hooks/index.ts @@ -7,3 +7,5 @@ export * from './use-disclosure'; export * from './use-notification'; export * from './use-rive-cat'; export * from './use-interval'; +export * from './use-minimize'; +export * from './use-always-on-top'; diff --git a/src/renderer/shared/hooks/use-always-on-top.ts b/src/renderer/shared/hooks/use-always-on-top.ts new file mode 100644 index 0000000..f956616 --- /dev/null +++ b/src/renderer/shared/hooks/use-always-on-top.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react'; + +export const useAlwaysOnTop = () => { + const [alwaysOnTop, setAlwaysOnTop] = useState(false); + + useEffect(() => { + const run = async () => { + const realAlwaysOnTop = await window.electronAPI.getAlwaysOnTop(); + if (realAlwaysOnTop !== alwaysOnTop) { + await window.electronAPI.setAlwaysOnTop(alwaysOnTop); + setAlwaysOnTop(alwaysOnTop); + } + }; + + run(); + }, [alwaysOnTop]); + + return { alwaysOnTop, setAlwaysOnTop }; +}; diff --git a/src/renderer/shared/hooks/use-minimize.ts b/src/renderer/shared/hooks/use-minimize.ts new file mode 100644 index 0000000..d97b2c5 --- /dev/null +++ b/src/renderer/shared/hooks/use-minimize.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react'; + +export const useMinimize = () => { + const [minimized, setMinimized] = useState(false); + + useEffect(() => { + const run = async () => { + const realMinimized = await window.electronAPI.getMinimized(); + if (realMinimized !== minimized) { + await window.electronAPI.setMinimized(minimized); + setMinimized(minimized); + } + }; + + run(); + }, [minimized]); + + return { minimized, setMinimized }; +}; diff --git a/src/renderer/shared/ui/dialog.tsx b/src/renderer/shared/ui/dialog.tsx index f06e1f9..dcd3a2e 100644 --- a/src/renderer/shared/ui/dialog.tsx +++ b/src/renderer/shared/ui/dialog.tsx @@ -30,7 +30,7 @@ export const Dialog = ({ (({ className, ...props }, ref) => ( )); diff --git a/src/renderer/shared/ui/icon.tsx b/src/renderer/shared/ui/icon.tsx index ab78031..50363ad 100644 --- a/src/renderer/shared/ui/icon.tsx +++ b/src/renderer/shared/ui/icon.tsx @@ -13,9 +13,13 @@ import ClockIcon from '@/shared/assets/svgs/clock.svg'; import CloseIcon from '@/shared/assets/svgs/close.svg'; import FocusTimeIcon from '@/shared/assets/svgs/focus-time.svg'; import MenuIcon from '@/shared/assets/svgs/menu.svg?react'; +import MinimizeOff from '@/shared/assets/svgs/minimize-off.svg'; +import MinimizeOn from '@/shared/assets/svgs/minimize-on.svg'; import MinusIcon from '@/shared/assets/svgs/minus.svg'; import MinusSvgIcon from '@/shared/assets/svgs/minus.svg?react'; import PenIcon from '@/shared/assets/svgs/pen.svg'; +import PinOff from '@/shared/assets/svgs/pin-off.svg'; +import PinOn from '@/shared/assets/svgs/pin-on.svg'; import PlaceholderIcon from '@/shared/assets/svgs/placeholder.svg'; import PlayIcon from '@/shared/assets/svgs/play.svg'; import PlusIcon from '@/shared/assets/svgs/plus.svg'; @@ -50,6 +54,10 @@ const icons = { clock: ClockIcon, readyForStat: ReadyForStatIcon, clockLine: ClockLineIcon, + pinOff: PinOff, + pinOn: PinOn, + minimizeOff: MinimizeOff, + minimizeOn: MinimizeOn, } as const; const sizes = { xs: 16, diff --git a/src/renderer/shared/ui/layouts.tsx b/src/renderer/shared/ui/layouts.tsx index fb351a0..ef484d6 100644 --- a/src/renderer/shared/ui/layouts.tsx +++ b/src/renderer/shared/ui/layouts.tsx @@ -10,7 +10,12 @@ export type SimpleLayoutProps = { }; export const SimpleLayout = ({ children }: SimpleLayoutProps) => { - return
{children}
; + return ( +
+
+
{children}
+
+ ); }; export type SidebarLayoutProps = { @@ -21,11 +26,11 @@ export type SidebarLayoutProps = { export const SidebarLayout = ({ title, children }: SidebarLayoutProps) => { return (
-
+
-
-

{title}

+
+

{title}

{children} diff --git a/src/renderer/widgets/pomodoro/ui/focus-screen.tsx b/src/renderer/widgets/pomodoro/ui/focus-screen.tsx index 5556bd0..aad7608 100644 --- a/src/renderer/widgets/pomodoro/ui/focus-screen.tsx +++ b/src/renderer/widgets/pomodoro/ui/focus-screen.tsx @@ -1,8 +1,9 @@ import { Time } from '@/features/time'; import { useUser } from '@/features/user'; +import hairballImage from '@/shared/assets/images/hairball.png'; import catFocusMotionRiveFile from '@/shared/assets/rivs/cat_focus.riv'; import { useRiveCat } from '@/shared/hooks'; -import { Button, Icon, Tooltip } from '@/shared/ui'; +import { Button, Icon, SimpleLayout, Tooltip } from '@/shared/ui'; import { cn, getCategoryIconName, msToTime } from '@/shared/utils'; type FocusScreenProps = { @@ -10,8 +11,12 @@ type FocusScreenProps = { currentFocusTime: number; elapsedTime: number; exceededTime: number; + minimized: boolean; + alwaysOnTop: boolean; handleRest: () => void; handleEnd: () => void; + setMinimized: (next: boolean) => void; + setAlwaysOnTop: (next: boolean) => void; }; const toolTipContentMap: Record = { @@ -24,8 +29,12 @@ export const FocusScreen = ({ currentFocusTime, elapsedTime, exceededTime, + minimized, + alwaysOnTop, handleRest, handleEnd, + setMinimized, + setAlwaysOnTop, }: FocusScreenProps) => { const isExceed = exceededTime > 0; const { minutes, seconds } = msToTime(currentFocusTime - elapsedTime); @@ -38,57 +47,125 @@ export const FocusScreen = ({ userCatType: user?.cat?.type, }); - return ( -
-
-
- - {currentCategory} + if (minimized) { + return ( +
+
+
+
+ + +
+
+
+
+

+ + {currentCategory} +

+
+
+
-
-
- - { - clickCatInput?.fire(); - }} - /> -
-
- - 집중시간 +
+ ); + } + + return ( + +
+
+
+ + {currentCategory} +
+
+
+ +
-
+
+ + { + clickCatInput?.fire(); + }} + /> +
+
+ + 집중시간 +
+
+
+ +
-
-
- -
-
+ ); }; diff --git a/src/renderer/widgets/pomodoro/ui/home-screen.tsx b/src/renderer/widgets/pomodoro/ui/home-screen.tsx index 92b14ab..5fd2a98 100644 --- a/src/renderer/widgets/pomodoro/ui/home-screen.tsx +++ b/src/renderer/widgets/pomodoro/ui/home-screen.tsx @@ -9,7 +9,7 @@ import { useUser } from '@/features/user'; import catHomeMotionRiveFile from '@/shared/assets/rivs/cat_home.riv'; import { LOCAL_STORAGE_KEY } from '@/shared/constants'; import { useDisclosure, useRiveCat } from '@/shared/hooks'; -import { Button, Guide, Icon, Tooltip, useToast } from '@/shared/ui'; +import { Button, Guide, Icon, SidebarLayout, Tooltip, useToast } from '@/shared/ui'; import { getCategoryIconName, createIsoDuration } from '@/shared/utils'; const steps = [ @@ -72,7 +72,7 @@ export const HomeScreen = ({ }; return ( - <> +
- + ); }; diff --git a/src/renderer/widgets/pomodoro/ui/rest-screen.tsx b/src/renderer/widgets/pomodoro/ui/rest-screen.tsx index be6ffa6..d7a8384 100644 --- a/src/renderer/widgets/pomodoro/ui/rest-screen.tsx +++ b/src/renderer/widgets/pomodoro/ui/rest-screen.tsx @@ -1,10 +1,11 @@ import { PomodoroNextAction } from '@/entities/pomodoro'; import { Time } from '@/features/time'; import { useUser } from '@/features/user'; +import hairballImage from '@/shared/assets/images/hairball.png'; import catRestMotionRiveFile from '@/shared/assets/rivs/cat_rest.riv'; import { MAX_REST_MINUTES, MIN_REST_MINUTES, MINUTES_GAP } from '@/shared/constants'; import { useRiveCat } from '@/shared/hooks'; -import { Button, Icon, SelectGroup, SelectGroupItem, Tooltip } from '@/shared/ui'; +import { Button, Icon, SelectGroup, SelectGroupItem, SimpleLayout, Tooltip } from '@/shared/ui'; import { cn, getCategoryIconName, msToTime } from '@/shared/utils'; type RestScreenProps = { @@ -14,9 +15,13 @@ type RestScreenProps = { exceededTime: number; currentRestMinutes: number; selectedNextAction: PomodoroNextAction | undefined; + minimized: boolean; + alwaysOnTop: boolean; setSelectedNextAction: (nextAction: PomodoroNextAction) => void; handleFocus: () => void; handleEnd: () => void; + setMinimized: (next: boolean) => void; + setAlwaysOnTop: (next: boolean) => void; }; export const RestScreen = ({ @@ -26,9 +31,13 @@ export const RestScreen = ({ exceededTime, currentRestMinutes, selectedNextAction, + minimized, + alwaysOnTop, setSelectedNextAction, handleFocus, handleEnd, + setMinimized, + setAlwaysOnTop, }: RestScreenProps) => { const isExceed = exceededTime > 0; const { minutes, seconds } = msToTime(currentRestTime - elapsedTime); @@ -41,80 +50,148 @@ export const RestScreen = ({ userCatType: user?.cat?.type, }); - return ( -
-
-
- - {currentCategory} -
-
- -
- - { - clickCatInput?.fire(); - }} - /> -
-
- - 휴식시간 + if (minimized) { + return ( +
+
+
+
+ +
-
+
+
+

+ + {currentCategory} +

+
+
+
+ ); + } -
-

다음부터 휴식시간을 바꿀까요?

- - +
+
+
+ + {currentCategory} +
+
+
+ +
-
+ + +
+ + +
+ + { + clickCatInput?.fire(); + }} + /> +
+
+ + 휴식시간 +
+
-
- - +
+

다음부터 휴식시간을 바꿀까요?

+ + + + 5분 + + = MAX_REST_MINUTES} + value="plus" + className="flex flex-row items-center justify-center gap-1 px-3 py-2" + > + + 5분 + + +
+
+ +
+ + +
-
+ ); }; diff --git a/src/renderer/widgets/pomodoro/ui/rest-wait-screen.tsx b/src/renderer/widgets/pomodoro/ui/rest-wait-screen.tsx index 7689747..0a8dc55 100644 --- a/src/renderer/widgets/pomodoro/ui/rest-wait-screen.tsx +++ b/src/renderer/widgets/pomodoro/ui/rest-wait-screen.tsx @@ -6,7 +6,7 @@ import { Time } from '@/features/time'; import completeFocusLottie from '@/shared/assets/lotties/loti_complete_focus.json?url'; import particleLottie from '@/shared/assets/lotties/loti_particle.json?url'; import { MAX_FOCUS_MINUTES, MIN_FOCUS_MINUTES, MINUTES_GAP } from '@/shared/constants'; -import { Button, Icon, SelectGroup, SelectGroupItem } from '@/shared/ui'; +import { Button, Icon, SelectGroup, SelectGroupItem, SimpleLayout } from '@/shared/ui'; import { msToTime } from '@/shared/utils'; type RestWaitScreenProps = { @@ -36,38 +36,31 @@ export const RestWaitScreen = ({ const { minutes: exceedMinutes, seconds: exceedSeconds } = msToTime(exceededTime); return ( -
-
-
-
- -
-
- - {isExceed && ( + +
+
+
+
+ +
+
- )} -
-
-

다음부터 집중시간을 바꿀까요?

- - - - 5분 - - = MAX_FOCUS_MINUTES} - value="plus" - className="flex flex-row items-center justify-center gap-1 px-3 py-2" + {isExceed && ( + + )} +
+
+

다음부터 집중시간을 바꿀까요?

+ - - 5분 - - + + + 5분 + + = MAX_FOCUS_MINUTES} + value="plus" + className="flex flex-row items-center justify-center gap-1 px-3 py-2" + > + + 5분 + + +
+
+
+ +
-
-
- -
-
+ ); }; diff --git a/src/shared/type.ts b/src/shared/type.ts index fd1d017..a326623 100644 --- a/src/shared/type.ts +++ b/src/shared/type.ts @@ -3,4 +3,8 @@ export interface IElectronAPI { showWindow: () => void; changeTrayIcon: (icon: string) => void; getMachineId: () => Promise; + getAlwaysOnTop: () => Promise; + setAlwaysOnTop: (isAlwaysOnTop: boolean) => Promise; + getMinimized: () => Promise; + setMinimized: (isMinimized: boolean) => Promise; }