Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/content/en/api-reference/overlay/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ await dmn.overlay.setLock(false);
Sets the fixed point used when resizing the overlay.

<Callout type="info">
Supported values: `top-left`, `top-right`, `bottom-left`, `bottom-right`, and
`center`.
Supported values: `top-left`, `top-right`, `bottom-left`, `bottom-right`,
`center`, and `fixed-position`.
</Callout>

```javascript
await dmn.overlay.setAnchor('top-left');
await dmn.overlay.setAnchor('center');
await dmn.overlay.setAnchor('fixed-position'); // Preserve existing elements' screen positions
await dmn.overlay.setAnchor('bottom-right');
```

Expand Down
3 changes: 2 additions & 1 deletion docs/content/en/api-reference/settings/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ interface SettingsState {
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'center';
| 'center'
| 'fixed-position';
keyCounterEnabled: boolean;
gridSettings: {
alignmentGuides: boolean;
Expand Down
9 changes: 9 additions & 0 deletions docs/content/en/guide/settings/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@ Select the program interface language:

Set the background color of the overlay window. Default is transparent.

If saving fails, the background returns to the saved color while the popup retains your input. After resolving the cause, click **Save again** to retry the same draft. The draft survives closing and reopening the popup, but is lost when you exit the app.

<Callout type="info">
Using window capture in OBS maintains transparent background. No chroma key
needed.
</Callout>

## Window Settings

### Overlay Visibility

The overlay starts closed on first launch. The app saves whether you show or hide it and restores that visibility on the next launch.

### Always on Top

Keeps the overlay window always above other windows. Useful when placing the key viewer over game screens.
Expand Down Expand Up @@ -70,6 +76,9 @@ Select the reference point when resizing the overlay window:
- **bottom-left**: Fixed bottom-left, expand top-right
- **bottom-right**: Fixed bottom-right, expand top-left
- **center**: Fixed center, expand all directions
- **fixed-position**: Preserve existing elements' screen positions when the window size or content origin changes

The default, top-left, fixes the window corner. When a sprite extends beyond the left or top edge and changes the content origin, existing elements can move on screen. Choose fixed-position to preserve their screen positions.

## Custom CSS/JS

Expand Down
3 changes: 2 additions & 1 deletion docs/content/ko/api-reference/overlay/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ await dmn.overlay.setLock(false); // 잠금 해제
리사이즈 기준점을 설정합니다.

<Callout type="info">
지원 값: `top-left`, `top-right`, `bottom-left`, `bottom-right`, `center`
지원 값: `top-left`, `top-right`, `bottom-left`, `bottom-right`, `center`, `fixed-position`
</Callout>

```javascript
await dmn.overlay.setAnchor("top-left"); // 좌상단 기준
await dmn.overlay.setAnchor("center"); // 중앙 기준
await dmn.overlay.setAnchor("fixed-position"); // 기존 요소의 화면 위치 유지
await dmn.overlay.setAnchor("bottom-right"); // 우하단 기준
```

Expand Down
3 changes: 2 additions & 1 deletion docs/content/ko/api-reference/settings/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ interface SettingsState {
| "top-right"
| "bottom-left"
| "bottom-right"
| "center";
| "center"
| "fixed-position";
keyCounterEnabled: boolean;
gridSettings: {
alignmentGuides: boolean;
Expand Down
9 changes: 9 additions & 0 deletions docs/content/ko/guide/settings/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ DM Note의 다양한 설정 옵션을 안내합니다.

오버레이 창의 배경색을 설정합니다. 기본값은 투명입니다.

저장에 실패하면 화면 배경은 저장된 값으로 복원되지만 팝업의 입력값은 유지됩니다. 오류 원인을 해소한 뒤 **다시 저장**을 누르면 같은 초안을 재시도할 수 있습니다. 초안은 팝업을 닫았다 다시 열어도 유지되지만 앱을 종료하면 사라집니다.

<Callout type="info">
OBS에서 윈도우 캡쳐를 사용하면 투명 배경이 유지됩니다. 크로마키가 필요 없습니다.
</Callout>

## 창 설정

### 오버레이 표시 상태

처음 실행할 때 오버레이는 닫힌 상태입니다. 오버레이를 켜거나 끈 상태를 저장하며, 다음 실행 때 마지막 표시 상태를 복원합니다.

### 항상 위에 표시

오버레이 창이 다른 창 위에 항상 표시되도록 합니다. 게임 화면 위에 키뷰어를 배치할 때 유용합니다.
Expand Down Expand Up @@ -69,6 +75,9 @@ DM Note의 다양한 설정 옵션을 안내합니다.
- **좌하단(bottom-left)**: 좌하단 고정, 우상단으로 확장
- **우하단(bottom-right)**: 우하단 고정, 좌상단으로 확장
- **중앙(center)**: 중앙 고정, 모든 방향으로 확장
- **고정(fixed-position)**: 창 크기와 콘텐츠 원점이 바뀌어도 기존 요소의 화면 위치 유지

기본값인 좌상단은 창의 모서리를 고정합니다. 스프라이트가 왼쪽이나 위쪽으로 넘쳐 콘텐츠 원점이 바뀌면 기존 요소의 화면 위치도 이동할 수 있습니다. 기존 요소의 화면 위치를 유지하려면 고정을 선택합니다.

## 커스텀 CSS/JS

Expand Down
33 changes: 30 additions & 3 deletions src/renderer/hooks/Modal/usePalette.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,38 @@ describe('배경색 저장 실패 복원', () => {
vi.restoreAllMocks();
});

it('거절된 낙관 배경색을 저장된 값으로 되돌리고 안내한다', async () => {
it('화면만 저장값으로 복원하고 팝업을 다시 열어도 초안과 오류를 유지한다', async () => {
mocks.update.mockRejectedValue(new Error('disk full'));
await act(async () => palette.handleColorChange('#222222'));

expect(useSettingsStore.getState().backgroundColor).toBe('#111111');
expect(palette.color).toBe('#111111');
expect(mocks.alert).toHaveBeenCalledTimes(1);
expect(palette.color).toBe('#222222');
expect(palette.saveError).toBe('common.saveFailed');
expect(palette.isSaving).toBe(false);
expect(mocks.alert).not.toHaveBeenCalled();
act(() => palette.setPalette(true));
act(() => palette.handlePaletteClose());
act(() => palette.setPalette(true));
expect(palette.color).toBe('#222222');
expect(palette.saveError).toBe('common.saveFailed');
});

it('쓰기 복구 후 같은 초안을 재시도하고 이후 외부 변경을 반영한다', async () => {
mocks.update
.mockRejectedValueOnce(new Error('disk full'))
.mockResolvedValue({});
await act(async () => palette.handleColorChange('#222222'));
await act(async () => palette.handleColorChange(palette.color));

expect(mocks.update).toHaveBeenCalledTimes(2);
expect(mocks.update).toHaveBeenLastCalledWith({
backgroundColor: '#222222',
});
expect(useSettingsStore.getState().backgroundColor).toBe('#222222');
expect(palette.saveError).toBeNull();
expect(palette.isSaving).toBe(false);
act(() => useSettingsStore.getState().setBackgroundColor('#555555'));
expect(palette.color).toBe('#555555');
});

it('이전 저장 실패가 나중에 선택한 배경색을 덮지 않는다', async () => {
Expand All @@ -75,6 +100,7 @@ describe('배경색 저장 실패 복원', () => {

expect(useSettingsStore.getState().backgroundColor).toBe('#333333');
expect(palette.color).toBe('#333333');
expect(palette.saveError).toBeNull();
});

it('저장값 재조회 중 도착한 외부 배경색을 덮지 않는다', async () => {
Expand All @@ -90,5 +116,6 @@ describe('배경색 저장 실패 복원', () => {
await act(async () => finishRead({ backgroundColor: '#111111' }));

expect(useSettingsStore.getState().backgroundColor).toBe('#444444');
expect(palette.color).toBe('#222222');
});
});
70 changes: 41 additions & 29 deletions src/renderer/hooks/Modal/usePalette.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,55 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useRef, useCallback } from 'react';
import { useSettingsStore } from '@stores/useSettingsStore';
import { settingsApi } from '@api/modules/app/settingsApi';
import { useTranslation } from '@contexts/useTranslation';

export function usePalette() {
const { t } = useTranslation();
const writeSequence = useRef(0);
const [saveError, setSaveError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [palette, setPalette] = useState(false);
const backgroundColor = useSettingsStore((state) => state.backgroundColor);
const setBackgroundColor = useSettingsStore(
(state) => state.setBackgroundColor,
);
const [color, setColor] = useState(backgroundColor);
const [draftColor, setDraftColor] = useState<string | null>(null);
const color = draftColor ?? backgroundColor;

useEffect(() => {
setColor(backgroundColor);
}, [backgroundColor]);

const handleColorChange = (newColor: string) => {
const previousColor = useSettingsStore.getState().backgroundColor;
const sequence = ++writeSequence.current;
setColor(newColor);
setBackgroundColor(newColor);
settingsApi.update({ backgroundColor: newColor }).catch(async (error) => {
console.error('Failed to update background color', error);
if (sequence !== writeSequence.current) return;
let restoredColor = previousColor;
try {
restoredColor = (await settingsApi.get()).backgroundColor;
} catch (syncError) {
console.error('Failed to reload background color', syncError);
}
if (sequence !== writeSequence.current) return;
if (useSettingsStore.getState().backgroundColor === newColor) {
setBackgroundColor(restoredColor);
}
void window.api.ui.dialog
.alert(t('common.saveFailed'), { confirmText: t('common.ok') })
.catch(() => {});
});
};
const handleColorChange = useCallback(
(newColor: string) => {
const previousColor = useSettingsStore.getState().backgroundColor;
const sequence = ++writeSequence.current;
setIsSaving(true);
setDraftColor(newColor);
setBackgroundColor(newColor);
settingsApi
.update({ backgroundColor: newColor })
.then(() => {
if (sequence !== writeSequence.current) return;
setSaveError(null);
setDraftColor(null);
setIsSaving(false);
})
.catch(async (error) => {
console.error('Failed to update background color', error);
if (sequence !== writeSequence.current) return;
let restoredColor = previousColor;
try {
restoredColor = (await settingsApi.get()).backgroundColor;
} catch (syncError) {
console.error('Failed to reload background color', syncError);
}
if (sequence !== writeSequence.current) return;
if (useSettingsStore.getState().backgroundColor === newColor) {
setBackgroundColor(restoredColor);
}
setSaveError(t('common.saveFailed'));
setIsSaving(false);
});
},
[setBackgroundColor, t],
);

const handlePaletteClose = () => {
if (palette) setPalette(false);
Expand All @@ -51,6 +61,8 @@ export function usePalette() {

return {
color,
saveError,
isSaving,
palette,
setPalette,
handleColorChange,
Expand Down
24 changes: 22 additions & 2 deletions src/renderer/windows/main/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,15 @@ export default function App() {
handleUndo,
handleRedo,
} = useKeyManager();
const { color, palette, setPalette, handleColorChange, handlePaletteClose } =
usePalette();
const {
color,
palette,
setPalette,
handleColorChange,
handlePaletteClose,
saveError,
isSaving,
} = usePalette();

const [activeTool, setActiveTool] = useState('move');
const [toolbarAddRequest, setToolbarAddRequest] = useState<{
Expand Down Expand Up @@ -501,6 +508,19 @@ export default function App() {
contentMountStrategy="after-paint"
>
<Palette color={color} onColorChange={handleColorChange} />
{saveError && (
<div className="mt-2 flex w-[142px] flex-col gap-2 text-xs">
<p role="alert">{saveError}</p>
<button
type="button"
className="rounded border px-2 py-1 disabled:opacity-50"
disabled={isSaving}
onClick={() => handleColorChange(color)}
>
{t(isSaving ? 'editorSave.retrying' : 'editorSave.retrySave')}
</button>
</div>
)}
</FloatingPopup>
{noteSettingPresence.mounted && shownNoteSettings && (
<NoteSettingModal
Expand Down
Loading