mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
notepad improvements
This commit is contained in:
parent
3c77be8dbb
commit
42b817949c
11 changed files with 308 additions and 49 deletions
|
|
@ -72,7 +72,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
|
||||||
bg={colorScheme === 'dark' ? 'dark.6' : 'gray.1'}
|
bg={colorScheme === 'dark' ? 'dark.6' : 'gray.1'}
|
||||||
>
|
>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Tooltip label="Notepad">
|
<Tooltip label="Notepad" disabled={isVisible}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={isVisible ? 'filled' : 'subtle'}
|
variant={isVisible ? 'filled' : 'subtle'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
31
src/components/Notepad/CloseConfirmModal.tsx
Normal file
31
src/components/Notepad/CloseConfirmModal.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Text, Button, Group } from '@mantine/core';
|
||||||
|
import { Modal } from '@/components/Modal';
|
||||||
|
|
||||||
|
interface CloseConfirmModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
tabTitle: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CloseConfirmModal = ({
|
||||||
|
isOpen,
|
||||||
|
tabTitle,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: CloseConfirmModalProps) => (
|
||||||
|
<Modal opened={isOpen} onClose={onCancel} title="Confirm Close Tab" size="sm">
|
||||||
|
<Text size="sm" mb="md">
|
||||||
|
The tab “{tabTitle}” contains content. Closing it will
|
||||||
|
permanently delete this data. Are you sure you want to close it?
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button variant="subtle" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={onConfirm}>
|
||||||
|
Close Tab
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { useEffect, useRef, useState, type MouseEvent } from 'react';
|
import { useEffect, useRef, useState, type MouseEvent } from 'react';
|
||||||
import { Box, Paper, ActionIcon } from '@mantine/core';
|
import { Box, Paper, ActionIcon } from '@mantine/core';
|
||||||
import { X, Plus } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { NOTEPAD_MIN_WIDTH, NOTEPAD_MIN_HEIGHT } from '@/constants/notepad';
|
import { NOTEPAD_MIN_WIDTH, NOTEPAD_MIN_HEIGHT } from '@/constants/notepad';
|
||||||
import { NotepadTabs } from './Tabs.tsx';
|
import { NotepadTabs } from './Tabs.tsx';
|
||||||
import { NotepadEditor } from './Editor.tsx';
|
import { NotepadEditor } from './Editor.tsx';
|
||||||
|
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
||||||
|
|
||||||
export const NotepadContainer = () => {
|
export const NotepadContainer = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -14,6 +15,7 @@ export const NotepadContainer = () => {
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
addTab,
|
addTab,
|
||||||
|
removeTab,
|
||||||
isLoaded,
|
isLoaded,
|
||||||
isVisible,
|
isVisible,
|
||||||
setVisible,
|
setVisible,
|
||||||
|
|
@ -22,6 +24,15 @@ export const NotepadContainer = () => {
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
||||||
|
const [confirmCloseModal, setConfirmCloseModal] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
tabId: string | null;
|
||||||
|
tabTitle: string;
|
||||||
|
}>({
|
||||||
|
isOpen: false,
|
||||||
|
tabId: null,
|
||||||
|
tabTitle: '',
|
||||||
|
});
|
||||||
|
|
||||||
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
||||||
|
|
||||||
|
|
@ -30,6 +41,32 @@ export const NotepadContainer = () => {
|
||||||
addTab(newTab);
|
addTab(newTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTabCloseRequest = (tabId: string) => {
|
||||||
|
const tab = tabs.find((t) => t.id === tabId);
|
||||||
|
if (!tab) return;
|
||||||
|
|
||||||
|
if (tab.content.trim().length > 0) {
|
||||||
|
setConfirmCloseModal({
|
||||||
|
isOpen: true,
|
||||||
|
tabId,
|
||||||
|
tabTitle: tab.title,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
removeTab(tabId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmClose = () => {
|
||||||
|
if (confirmCloseModal.tabId) {
|
||||||
|
removeTab(confirmCloseModal.tabId);
|
||||||
|
}
|
||||||
|
setConfirmCloseModal({ isOpen: false, tabId: null, tabTitle: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelClose = () => {
|
||||||
|
setConfirmCloseModal({ isOpen: false, tabId: null, tabTitle: '' });
|
||||||
|
};
|
||||||
|
|
||||||
const handleResizeStart =
|
const handleResizeStart =
|
||||||
(direction: string) => (e: MouseEvent<HTMLDivElement>) => {
|
(direction: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -87,11 +124,11 @@ export const NotepadContainer = () => {
|
||||||
withBorder
|
withBorder
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: 5,
|
left: 0,
|
||||||
bottom: 5,
|
bottom: 24,
|
||||||
width: position.width,
|
width: position.width,
|
||||||
height: position.height,
|
height: position.height,
|
||||||
zIndex: 1000,
|
zIndex: 100,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
resolvedColorScheme === 'dark'
|
resolvedColorScheme === 'dark'
|
||||||
? 'var(--mantine-color-dark-6)'
|
? 'var(--mantine-color-dark-6)'
|
||||||
|
|
@ -132,11 +169,6 @@ export const NotepadContainer = () => {
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
cursor: 'ne-resize',
|
cursor: 'ne-resize',
|
||||||
backgroundColor:
|
|
||||||
resolvedColorScheme === 'dark'
|
|
||||||
? 'var(--mantine-color-dark-5)'
|
|
||||||
: 'var(--mantine-color-gray-4)',
|
|
||||||
borderRadius: '2px',
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleResizeStart('top-right')}
|
onMouseDown={handleResizeStart('top-right')}
|
||||||
/>
|
/>
|
||||||
|
|
@ -155,7 +187,10 @@ export const NotepadContainer = () => {
|
||||||
minHeight: 28,
|
minHeight: 28,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NotepadTabs />
|
<NotepadTabs
|
||||||
|
onCreateNewTab={handleCreateNewTab}
|
||||||
|
onCloseTab={handleTabCloseRequest}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -166,10 +201,6 @@ export const NotepadContainer = () => {
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon variant="subtle" size="xs" onClick={handleCreateNewTab}>
|
|
||||||
<Plus size={12} />
|
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
|
@ -185,6 +216,13 @@ export const NotepadContainer = () => {
|
||||||
{activeTab && <NotepadEditor tab={activeTab} />}
|
{activeTab && <NotepadEditor tab={activeTab} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<CloseConfirmModal
|
||||||
|
isOpen={confirmCloseModal.isOpen}
|
||||||
|
tabTitle={confirmCloseModal.tabTitle}
|
||||||
|
onConfirm={handleConfirmClose}
|
||||||
|
onCancel={handleCancelClose}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
type MouseEvent,
|
||||||
|
} from 'react';
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { search } from '@codemirror/search';
|
import { search } from '@codemirror/search';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
import { history, historyKeymap } from '@codemirror/commands';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import type { NotepadTab } from '@/types/electron';
|
import type { NotepadTab } from '@/types/electron';
|
||||||
|
|
@ -13,7 +20,8 @@ interface NotepadEditorProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
const { saveTabContent } = useNotepadStore();
|
const { saveTabContent, showLineNumbers, setShowLineNumbers } =
|
||||||
|
useNotepadStore();
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
const { resolvedColorScheme } = usePreferencesStore();
|
||||||
const [content, setContent] = useState(tab.content);
|
const [content, setContent] = useState(tab.content);
|
||||||
const [saveTimeout, setSaveTimeout] = useState<ReturnType<
|
const [saveTimeout, setSaveTimeout] = useState<ReturnType<
|
||||||
|
|
@ -38,6 +46,14 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
[tab.id, saveTabContent, saveTimeout]
|
[tab.id, saveTabContent, saveTimeout]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleEditorContextMenu = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
|
if (target.closest('.cm-gutters') || target.closest('.cm-lineNumbers')) {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowLineNumbers(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleBoxClick = useCallback(() => {
|
const handleBoxClick = useCallback(() => {
|
||||||
if (editorRef.current?.view) {
|
if (editorRef.current?.view) {
|
||||||
editorRef.current.view.focus();
|
editorRef.current.view.focus();
|
||||||
|
|
@ -59,6 +75,9 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
search(),
|
search(),
|
||||||
|
history(),
|
||||||
|
keymap.of(historyKeymap),
|
||||||
|
highlightActiveLine(),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
|
|
@ -72,12 +91,13 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
h="100%"
|
h="100%"
|
||||||
|
onClick={handleBoxClick}
|
||||||
|
onContextMenu={handleEditorContextMenu}
|
||||||
style={{
|
style={{
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
onClick={handleBoxClick}
|
|
||||||
>
|
>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
|
@ -86,7 +106,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
theme={theme}
|
theme={theme}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: true,
|
lineNumbers: showLineNumbers,
|
||||||
foldGutter: false,
|
foldGutter: false,
|
||||||
dropCursor: false,
|
dropCursor: false,
|
||||||
allowMultipleSelections: false,
|
allowMultipleSelections: false,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
import { type MouseEvent, type DragEvent, useState } from 'react';
|
import {
|
||||||
import { Box, ActionIcon, Text } from '@mantine/core';
|
type MouseEvent,
|
||||||
import { X } from 'lucide-react';
|
type DragEvent,
|
||||||
|
type KeyboardEvent,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import { Box, ActionIcon, Text, TextInput } from '@mantine/core';
|
||||||
|
import { X, Plus } from 'lucide-react';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
|
interface NotepadTabsProps {
|
||||||
|
onCreateNewTab: () => Promise<void>;
|
||||||
|
onCloseTab: (tabId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
id: string;
|
id: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -14,7 +26,10 @@ interface TabProps {
|
||||||
onDragStart: (e: DragEvent, index: number) => void;
|
onDragStart: (e: DragEvent, index: number) => void;
|
||||||
onDragOver: (e: DragEvent) => void;
|
onDragOver: (e: DragEvent) => void;
|
||||||
onDrop: (e: DragEvent, index: number) => void;
|
onDrop: (e: DragEvent, index: number) => void;
|
||||||
|
onRename: (newTitle: string) => void;
|
||||||
isDragOver: boolean;
|
isDragOver: boolean;
|
||||||
|
showLineNumbers: boolean;
|
||||||
|
setShowLineNumbers: (show: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = ({
|
const Tab = ({
|
||||||
|
|
@ -26,14 +41,77 @@ const Tab = ({
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
onDrop,
|
onDrop,
|
||||||
|
onRename,
|
||||||
isDragOver,
|
isDragOver,
|
||||||
|
showLineNumbers,
|
||||||
|
setShowLineNumbers,
|
||||||
}: TabProps) => {
|
}: TabProps) => {
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
const { resolvedColorScheme } = usePreferencesStore();
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editingTitle, setEditingTitle] = useState(title);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
}
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const handleTitleClick = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!isActive) {
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTitleDoubleClick = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isActive) {
|
||||||
|
setIsEditing(true);
|
||||||
|
setEditingTitle(title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSaveTitle();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditingTitle(title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
handleSaveTitle();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveTitle = () => {
|
||||||
|
const trimmedTitle = editingTitle.trim();
|
||||||
|
if (trimmedTitle && trimmedTitle !== title) {
|
||||||
|
onRename(trimmedTitle);
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabClick = () => {
|
||||||
|
if (!isEditing) {
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
if (e.button === 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
draggable
|
draggable={!isEditing}
|
||||||
onClick={onSelect}
|
onClick={handleTabClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
onDragStart={(e) => onDragStart(e, index)}
|
onDragStart={(e) => onDragStart(e, index)}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={(e) => onDrop(e, index)}
|
onDrop={(e) => onDrop(e, index)}
|
||||||
|
|
@ -53,7 +131,7 @@ const Tab = ({
|
||||||
? 'var(--mantine-color-dark-4)'
|
? 'var(--mantine-color-dark-4)'
|
||||||
: 'var(--mantine-color-gray-3)'
|
: 'var(--mantine-color-gray-3)'
|
||||||
}`,
|
}`,
|
||||||
cursor: 'pointer',
|
cursor: isEditing ? 'default' : 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
|
|
@ -62,17 +140,49 @@ const Tab = ({
|
||||||
opacity: isDragOver ? 0.5 : 1,
|
opacity: isDragOver ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
{isEditing ? (
|
||||||
size="xs"
|
<TextInput
|
||||||
style={{
|
ref={inputRef}
|
||||||
overflow: 'hidden',
|
value={editingTitle}
|
||||||
textOverflow: 'ellipsis',
|
onChange={(e) => setEditingTitle(e.target.value)}
|
||||||
whiteSpace: 'nowrap',
|
onKeyDown={handleInputKeyDown}
|
||||||
flex: 1,
|
onBlur={handleInputBlur}
|
||||||
}}
|
size="xs"
|
||||||
>
|
variant="unstyled"
|
||||||
{title}
|
style={{
|
||||||
</Text>
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
fontSize: 'var(--mantine-font-size-xs)',
|
||||||
|
padding: 0,
|
||||||
|
minHeight: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
lineHeight: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
onClick={handleTitleClick}
|
||||||
|
onDoubleClick={handleTitleDoubleClick}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowLineNumbers(!showLineNumbers);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<ActionIcon variant="subtle" size="xs" onClick={onClose}>
|
<ActionIcon variant="subtle" size="xs" onClick={onClose}>
|
||||||
<X size={10} />
|
<X size={10} />
|
||||||
|
|
@ -81,9 +191,19 @@ const Tab = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotepadTabs = () => {
|
export const NotepadTabs = ({
|
||||||
const { tabs, activeTabId, setActiveTab, removeTab, reorderTabs } =
|
onCreateNewTab,
|
||||||
useNotepadStore();
|
onCloseTab,
|
||||||
|
}: NotepadTabsProps) => {
|
||||||
|
const {
|
||||||
|
tabs,
|
||||||
|
activeTabId,
|
||||||
|
setActiveTab,
|
||||||
|
reorderTabs,
|
||||||
|
updateTab,
|
||||||
|
showLineNumbers,
|
||||||
|
setShowLineNumbers,
|
||||||
|
} = useNotepadStore();
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
const { resolvedColorScheme } = usePreferencesStore();
|
||||||
const [draggedTabIndex, setDraggedTabIndex] = useState<number | null>(null);
|
const [draggedTabIndex, setDraggedTabIndex] = useState<number | null>(null);
|
||||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||||
|
|
@ -94,7 +214,19 @@ export const NotepadTabs = () => {
|
||||||
|
|
||||||
const handleTabClose = (e: MouseEvent, tabId: string) => {
|
const handleTabClose = (e: MouseEvent, tabId: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
removeTab(tabId);
|
onCloseTab(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabRename = (tabId: string, newTitle: string) => {
|
||||||
|
updateTab(tabId, { title: newTitle });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabBarContextMenu = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!showLineNumbers) {
|
||||||
|
setShowLineNumbers(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragStart = (e: DragEvent, index: number) => {
|
const handleDragStart = (e: DragEvent, index: number) => {
|
||||||
|
|
@ -130,6 +262,7 @@ export const NotepadTabs = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
onContextMenu={handleTabBarContextMenu}
|
||||||
style={{
|
style={{
|
||||||
borderBottom: `1px solid ${
|
borderBottom: `1px solid ${
|
||||||
resolvedColorScheme === 'dark'
|
resolvedColorScheme === 'dark'
|
||||||
|
|
@ -154,13 +287,28 @@ export const NotepadTabs = () => {
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
onSelect={() => handleTabSelect(tab.id)}
|
onSelect={() => handleTabSelect(tab.id)}
|
||||||
onClose={(e) => handleTabClose(e, tab.id)}
|
onClose={(e) => handleTabClose(e, tab.id)}
|
||||||
|
onRename={(newTitle) => handleTabRename(tab.id, newTitle)}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
isDragOver={dragOverIndex === index}
|
isDragOver={dragOverIndex === index}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
setShowLineNumbers={setShowLineNumbers}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
onClick={onCreateNewTab}
|
||||||
|
style={{
|
||||||
|
margin: '4px',
|
||||||
|
alignSelf: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={12} />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
export const DEFAULT_NOTEPAD_POSITION = {
|
export const DEFAULT_NOTEPAD_POSITION = {
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 400,
|
height: 400,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
|
||||||
if (totalBytes > 0) {
|
if (totalBytes > 0) {
|
||||||
const progress = (downloadedBytes / totalBytes) * 100;
|
const progress = (downloadedBytes / totalBytes) * 100;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (now - lastProgressUpdate > 100) {
|
if (now - lastProgressUpdate > 100) {
|
||||||
mainWindow.webContents.send('download-progress', progress);
|
mainWindow.webContents.send('download-progress', progress);
|
||||||
lastProgressUpdate = now;
|
lastProgressUpdate = now;
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,16 @@ const appAPI: AppAPI = {
|
||||||
ipcRenderer.removeListener('window-unmaximized', handler);
|
ipcRenderer.removeListener('window-unmaximized', handler);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
onLineNumbersChanged: (callback) => {
|
||||||
|
const handler = (_: IpcRendererEvent, showLineNumbers: boolean) =>
|
||||||
|
callback(showLineNumbers);
|
||||||
|
|
||||||
|
ipcRenderer.on('line-numbers-changed', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener('line-numbers-changed', handler);
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const configAPI: ConfigAPI = {
|
const configAPI: ConfigAPI = {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ interface NotepadStore extends NotepadState {
|
||||||
reorderTabs: (fromIndex: number, toIndex: number) => void;
|
reorderTabs: (fromIndex: number, toIndex: number) => void;
|
||||||
setPosition: (position: NotepadState['position']) => void;
|
setPosition: (position: NotepadState['position']) => void;
|
||||||
setVisible: (visible: boolean) => void;
|
setVisible: (visible: boolean) => void;
|
||||||
|
setShowLineNumbers: (showLineNumbers: boolean) => void;
|
||||||
loadState: () => Promise<void>;
|
loadState: () => Promise<void>;
|
||||||
saveState: () => Promise<void>;
|
saveState: () => Promise<void>;
|
||||||
saveTabContent: (tabId: string, content: string) => Promise<void>;
|
saveTabContent: (tabId: string, content: string) => Promise<void>;
|
||||||
|
|
@ -24,6 +25,7 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
activeTabId: null,
|
activeTabId: null,
|
||||||
position: DEFAULT_NOTEPAD_POSITION,
|
position: DEFAULT_NOTEPAD_POSITION,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
|
showLineNumbers: true,
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
|
|
||||||
setTabs: (tabs) => set({ tabs }),
|
setTabs: (tabs) => set({ tabs }),
|
||||||
|
|
@ -85,6 +87,10 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
set({ isVisible: visible });
|
set({ isVisible: visible });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setShowLineNumbers: (showLineNumbers) => {
|
||||||
|
set({ showLineNumbers });
|
||||||
|
},
|
||||||
|
|
||||||
loadState: async () => {
|
loadState: async () => {
|
||||||
try {
|
try {
|
||||||
const savedState = await window.electronAPI.notepad.loadState();
|
const savedState = await window.electronAPI.notepad.loadState();
|
||||||
|
|
@ -106,6 +112,7 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
activeTabId: savedState.activeTabId || tabsWithContent[0]?.id || null,
|
activeTabId: savedState.activeTabId || tabsWithContent[0]?.id || null,
|
||||||
position: savedState.position,
|
position: savedState.position,
|
||||||
isVisible: savedState.isVisible,
|
isVisible: savedState.isVisible,
|
||||||
|
showLineNumbers: savedState.showLineNumbers ?? true,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -132,6 +139,7 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
activeTabId: state.activeTabId,
|
activeTabId: state.activeTabId,
|
||||||
position: state.position,
|
position: state.position,
|
||||||
isVisible: state.isVisible,
|
isVisible: state.isVisible,
|
||||||
|
showLineNumbers: state.showLineNumbers,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -148,6 +156,7 @@ useNotepadStore.subscribe(
|
||||||
activeTabId: state.activeTabId,
|
activeTabId: state.activeTabId,
|
||||||
position: state.position,
|
position: state.position,
|
||||||
isVisible: state.isVisible,
|
isVisible: state.isVisible,
|
||||||
|
showLineNumbers: state.showLineNumbers,
|
||||||
}),
|
}),
|
||||||
() => {
|
() => {
|
||||||
if (useNotepadStore.getState().isLoaded) {
|
if (useNotepadStore.getState().isLoaded) {
|
||||||
|
|
@ -159,7 +168,8 @@ useNotepadStore.subscribe(
|
||||||
a.tabs === b.tabs &&
|
a.tabs === b.tabs &&
|
||||||
a.activeTabId === b.activeTabId &&
|
a.activeTabId === b.activeTabId &&
|
||||||
a.position === b.position &&
|
a.position === b.position &&
|
||||||
a.isVisible === b.isVisible,
|
a.isVisible === b.isVisible &&
|
||||||
|
a.showLineNumbers === b.showLineNumbers,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
9
src/types/electron.d.ts
vendored
9
src/types/electron.d.ts
vendored
|
|
@ -174,6 +174,9 @@ export interface AppAPI {
|
||||||
quitAndInstall: () => Promise<void>;
|
quitAndInstall: () => Promise<void>;
|
||||||
isUpdateDownloaded: () => Promise<boolean>;
|
isUpdateDownloaded: () => Promise<boolean>;
|
||||||
onWindowStateToggle: (callback: () => void) => () => void;
|
onWindowStateToggle: (callback: () => void) => () => void;
|
||||||
|
onLineNumbersChanged: (
|
||||||
|
callback: (showLineNumbers: boolean) => void
|
||||||
|
) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigAPI {
|
export interface ConfigAPI {
|
||||||
|
|
@ -221,24 +224,22 @@ export interface NotepadState {
|
||||||
tabs: NotepadTab[];
|
tabs: NotepadTab[];
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
position: {
|
position: {
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
showLineNumbers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedNotepadState {
|
export interface SavedNotepadState {
|
||||||
tabs: SavedNotepadTab[];
|
tabs: SavedNotepadTab[];
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
position: {
|
position: {
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotepadAPI {
|
export interface NotepadAPI {
|
||||||
|
|
|
||||||
4
src/types/ipc.d.ts
vendored
4
src/types/ipc.d.ts
vendored
|
|
@ -4,7 +4,8 @@ export type IPCChannel =
|
||||||
| 'versions-updated'
|
| 'versions-updated'
|
||||||
| 'kobold-output'
|
| 'kobold-output'
|
||||||
| 'window-maximized'
|
| 'window-maximized'
|
||||||
| 'window-unmaximized';
|
| 'window-unmaximized'
|
||||||
|
| 'line-numbers-changed';
|
||||||
|
|
||||||
export interface IPCChannelPayloads {
|
export interface IPCChannelPayloads {
|
||||||
'download-progress': [progress: number];
|
'download-progress': [progress: number];
|
||||||
|
|
@ -13,4 +14,5 @@ export interface IPCChannelPayloads {
|
||||||
'kobold-output': [message: string];
|
'kobold-output': [message: string];
|
||||||
'window-maximized': [];
|
'window-maximized': [];
|
||||||
'window-unmaximized': [];
|
'window-unmaximized': [];
|
||||||
|
'line-numbers-changed': [showLineNumbers: boolean];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue