mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
unify notepad config with the app config
This commit is contained in:
parent
70e9fb1eb8
commit
4200138478
10 changed files with 197 additions and 179 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"productName": "Gerbil",
|
"productName": "Gerbil",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
|
|
|
||||||
|
|
@ -26,45 +26,40 @@ export const NotepadContainer = () => {
|
||||||
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
||||||
const [confirmCloseModal, setConfirmCloseModal] = useState<{
|
const [confirmCloseModal, setConfirmCloseModal] = useState<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
tabId: string | null;
|
title: string;
|
||||||
tabTitle: string;
|
|
||||||
}>({
|
}>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
tabId: null,
|
title: '',
|
||||||
tabTitle: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
const activeTab = tabs.find((tab) => tab.title === activeTabId);
|
||||||
|
|
||||||
const handleCreateNewTab = async () => {
|
const handleCreateNewTab = async () => {
|
||||||
const newTab = await window.electronAPI.notepad.createNewTab();
|
const newTab = await window.electronAPI.notepad.createNewTab();
|
||||||
addTab(newTab);
|
addTab(newTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabCloseRequest = (tabId: string) => {
|
const handleTabCloseRequest = (title: string) => {
|
||||||
const tab = tabs.find((t) => t.id === tabId);
|
const tab = tabs.find((t) => t.title === title);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
if (tab.content.trim().length > 0) {
|
if (tab.content.trim().length > 0) {
|
||||||
setConfirmCloseModal({
|
setConfirmCloseModal({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
tabId,
|
title,
|
||||||
tabTitle: tab.title,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
removeTab(tabId);
|
removeTab(title);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmClose = () => {
|
const handleConfirmClose = () => {
|
||||||
if (confirmCloseModal.tabId) {
|
removeTab(confirmCloseModal.title);
|
||||||
removeTab(confirmCloseModal.tabId);
|
setConfirmCloseModal({ isOpen: false, title: '' });
|
||||||
}
|
|
||||||
setConfirmCloseModal({ isOpen: false, tabId: null, tabTitle: '' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelClose = () => {
|
const handleCancelClose = () => {
|
||||||
setConfirmCloseModal({ isOpen: false, tabId: null, tabTitle: '' });
|
setConfirmCloseModal({ isOpen: false, title: '' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResizeStart =
|
const handleResizeStart =
|
||||||
|
|
@ -207,7 +202,7 @@ export const NotepadContainer = () => {
|
||||||
onClick={() => setVisible(false)}
|
onClick={() => setVisible(false)}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -219,7 +214,7 @@ export const NotepadContainer = () => {
|
||||||
|
|
||||||
<CloseConfirmModal
|
<CloseConfirmModal
|
||||||
isOpen={confirmCloseModal.isOpen}
|
isOpen={confirmCloseModal.isOpen}
|
||||||
tabTitle={confirmCloseModal.tabTitle}
|
tabTitle={confirmCloseModal.title}
|
||||||
onConfirm={handleConfirmClose}
|
onConfirm={handleConfirmClose}
|
||||||
onCancel={handleCancelClose}
|
onCancel={handleCancelClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,12 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
saveTabContent(tab.id, newContent);
|
saveTabContent(tab.title, newContent);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
setSaveTimeout(timeout);
|
setSaveTimeout(timeout);
|
||||||
},
|
},
|
||||||
[tab.id, saveTabContent, saveTimeout]
|
[tab.title, saveTabContent, saveTimeout]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEditorContextMenu = (e: MouseEvent) => {
|
const handleEditorContextMenu = (e: MouseEvent) => {
|
||||||
|
|
@ -62,7 +62,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContent(tab.content);
|
setContent(tab.content);
|
||||||
}, [tab.content, tab.id]);
|
}, [tab.content, tab.title]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,13 @@ import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
interface NotepadTabsProps {
|
interface NotepadTabsProps {
|
||||||
onCreateNewTab: () => Promise<void>;
|
onCreateNewTab: () => Promise<void>;
|
||||||
onCloseTab: (tabId: string) => void;
|
onCloseTab: (title: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
id: string;
|
title: string;
|
||||||
index: number;
|
index: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
title: string;
|
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onClose: (e: MouseEvent) => void;
|
onClose: (e: MouseEvent) => void;
|
||||||
onDragStart: (e: DragEvent, index: number) => void;
|
onDragStart: (e: DragEvent, index: number) => void;
|
||||||
|
|
@ -166,6 +165,7 @@ const Tab = ({
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
size="xs"
|
size="xs"
|
||||||
|
title={title}
|
||||||
onClick={handleTitleClick}
|
onClick={handleTitleClick}
|
||||||
onDoubleClick={handleTitleDoubleClick}
|
onDoubleClick={handleTitleDoubleClick}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
|
|
@ -208,17 +208,18 @@ export const NotepadTabs = ({
|
||||||
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);
|
||||||
|
|
||||||
const handleTabSelect = (tabId: string) => {
|
const handleTabSelect = (title: string) => {
|
||||||
setActiveTab(tabId);
|
setActiveTab(title);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabClose = (e: MouseEvent, tabId: string) => {
|
const handleTabClose = (e: MouseEvent, title: string) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onCloseTab(tabId);
|
onCloseTab(title);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabRename = (tabId: string, newTitle: string) => {
|
const handleTabRename = (title: string, newTitle: string) => {
|
||||||
updateTab(tabId, { title: newTitle });
|
updateTab(title, { title: newTitle });
|
||||||
|
window.electronAPI.notepad.renameTab(title, newTitle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabBarContextMenu = (e: MouseEvent) => {
|
const handleTabBarContextMenu = (e: MouseEvent) => {
|
||||||
|
|
@ -258,8 +259,6 @@ export const NotepadTabs = ({
|
||||||
setDragOverIndex(null);
|
setDragOverIndex(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tabs.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
onContextMenu={handleTabBarContextMenu}
|
onContextMenu={handleTabBarContextMenu}
|
||||||
|
|
@ -276,18 +275,17 @@ export const NotepadTabs = ({
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<Box
|
<Box
|
||||||
key={tab.id}
|
key={tab.title}
|
||||||
onDragEnter={() => handleDragEnter(index)}
|
onDragEnter={() => handleDragEnter(index)}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
id={tab.id}
|
|
||||||
index={index}
|
|
||||||
isActive={tab.id === activeTabId}
|
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
onSelect={() => handleTabSelect(tab.id)}
|
index={index}
|
||||||
onClose={(e) => handleTabClose(e, tab.id)}
|
isActive={tab.title === activeTabId}
|
||||||
onRename={(newTitle) => handleTabRename(tab.id, newTitle)}
|
onSelect={() => handleTabSelect(tab.title)}
|
||||||
|
onClose={(e) => handleTabClose(e, tab.title)}
|
||||||
|
onRename={(newTitle) => handleTabRename(tab.title, newTitle)}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
|
|
@ -303,7 +301,7 @@ export const NotepadTabs = ({
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onCreateNewTab}
|
onClick={onCreateNewTab}
|
||||||
style={{
|
style={{
|
||||||
margin: '4px',
|
margin: '0.25rem',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import {
|
||||||
loadNotepadState,
|
loadNotepadState,
|
||||||
deleteTabFile,
|
deleteTabFile,
|
||||||
createNewTab,
|
createNewTab,
|
||||||
|
renameTab,
|
||||||
} from '@/main/modules/notepad';
|
} from '@/main/modules/notepad';
|
||||||
import {
|
import {
|
||||||
detectGPU,
|
detectGPU,
|
||||||
|
|
@ -279,11 +280,15 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'notepad:saveTabContent',
|
'notepad:saveTabContent',
|
||||||
(_, tabId: string, content: string) => saveTabContent(tabId, content)
|
(_, title: string, content: string) => saveTabContent(title, content)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('notepad:loadTabContent', (_, tabId: string) =>
|
ipcMain.handle('notepad:loadTabContent', (_, title: string) =>
|
||||||
loadTabContent(tabId)
|
loadTabContent(title)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('notepad:renameTab', (_, oldTitle: string, newTitle: string) =>
|
||||||
|
renameTab(oldTitle, newTitle)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('notepad:saveState', (_, state: NotepadState) =>
|
ipcMain.handle('notepad:saveState', (_, state: NotepadState) =>
|
||||||
|
|
@ -292,8 +297,8 @@ export function setupIPCHandlers() {
|
||||||
|
|
||||||
ipcMain.handle('notepad:loadState', () => loadNotepadState());
|
ipcMain.handle('notepad:loadState', () => loadNotepadState());
|
||||||
|
|
||||||
ipcMain.handle('notepad:deleteTab', (_, tabId: string) =>
|
ipcMain.handle('notepad:deleteTab', (_, title: string) =>
|
||||||
deleteTabFile(tabId)
|
deleteTabFile(title)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('notepad:createNewTab', (_, title?: string) =>
|
ipcMain.handle('notepad:createNewTab', (_, title?: string) =>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { nativeTheme } from 'electron';
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type { FrontendPreference } from '@/types';
|
import type { FrontendPreference } from '@/types';
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
|
import type { SavedNotepadState } from '@/types/electron';
|
||||||
|
|
||||||
interface WindowBounds {
|
interface WindowBounds {
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -28,6 +29,7 @@ interface AppConfig {
|
||||||
skipEjectConfirmation?: boolean;
|
skipEjectConfirmation?: boolean;
|
||||||
dismissedUpdates?: string[];
|
dismissedUpdates?: string[];
|
||||||
zoomLevel?: number;
|
zoomLevel?: number;
|
||||||
|
notepad?: SavedNotepadState;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: AppConfig = {};
|
let config: AppConfig = {};
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,119 @@
|
||||||
import { promises as fs } from 'fs';
|
import { get, set, getInstallDir } from './config';
|
||||||
import { join } from 'path';
|
import type { SavedNotepadState } from '@/types/electron';
|
||||||
import { safeExecute, tryExecute } from '@/utils/node/logging';
|
|
||||||
import { getInstallDir } from './config';
|
|
||||||
import type { SavedNotepadState, SavedNotepadTab } from '@/types/electron';
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_NOTEPAD_POSITION,
|
DEFAULT_NOTEPAD_POSITION,
|
||||||
DEFAULT_TAB_CONTENT,
|
DEFAULT_TAB_CONTENT,
|
||||||
} from '@/constants/notepad';
|
} from '@/constants/notepad';
|
||||||
|
import { pathExists } from '@/utils/node/fs';
|
||||||
const NOTEPAD_DIR = join(getInstallDir(), 'notepad');
|
import { join } from 'path';
|
||||||
const NOTEPAD_STATE_FILE = join(NOTEPAD_DIR, 'state.json');
|
import { readFile, readdir, writeFile, unlink, rename } from 'fs/promises';
|
||||||
|
|
||||||
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
|
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
|
||||||
tabs: [],
|
|
||||||
activeTabId: null,
|
activeTabId: null,
|
||||||
position: DEFAULT_NOTEPAD_POSITION,
|
position: DEFAULT_NOTEPAD_POSITION,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function ensureNotepadDir() {
|
const getNotepadDir = () => join(getInstallDir(), 'notepad');
|
||||||
return tryExecute(async () => {
|
|
||||||
await fs.mkdir(NOTEPAD_DIR, { recursive: true });
|
|
||||||
}, 'Failed to create notepad directory');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveTabContent(tabId: string, content: string) {
|
const getTabsFromStorage = async () => {
|
||||||
return tryExecute(async () => {
|
const tabs = [];
|
||||||
await ensureNotepadDir();
|
|
||||||
const filePath = join(NOTEPAD_DIR, `${tabId}.txt`);
|
|
||||||
await fs.writeFile(filePath, content, 'utf8');
|
|
||||||
}, 'Failed to save tab content');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTabContent(tabId: string) {
|
|
||||||
const filePath = join(NOTEPAD_DIR, `${tabId}.txt`);
|
|
||||||
try {
|
try {
|
||||||
return fs.readFile(filePath, 'utf8');
|
const notepadDir = getNotepadDir();
|
||||||
} catch (error) {
|
if (await pathExists(notepadDir)) {
|
||||||
if ((error as { code?: string }).code === 'ENOENT') {
|
const files = await readdir(notepadDir);
|
||||||
|
const tabFiles = files.filter((f) => f.endsWith('.txt'));
|
||||||
|
|
||||||
|
for (const file of tabFiles) {
|
||||||
|
const title = file.replace('.txt', '');
|
||||||
|
tabs.push({ title });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renameTab = async (oldTitle: string, newTitle: string) => {
|
||||||
|
try {
|
||||||
|
const notepadDir = getNotepadDir();
|
||||||
|
|
||||||
|
await rename(
|
||||||
|
join(notepadDir, `${oldTitle}.txt`),
|
||||||
|
join(notepadDir, `${newTitle}.txt`)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveTabContent = async (title: string, content: string) => {
|
||||||
|
try {
|
||||||
|
const notepadDir = getNotepadDir();
|
||||||
|
const filePath = join(notepadDir, `${title}.txt`);
|
||||||
|
|
||||||
|
await writeFile(filePath, content, 'utf-8');
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTabContent = async (title: string) => {
|
||||||
|
try {
|
||||||
|
const notepadDir = getNotepadDir();
|
||||||
|
return readFile(join(notepadDir, `${title}.txt`), 'utf-8');
|
||||||
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
throw error;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveNotepadState(state: SavedNotepadState) {
|
export const saveNotepadState = async (state: SavedNotepadState) => {
|
||||||
return tryExecute(async () => {
|
await set('notepad', state);
|
||||||
await ensureNotepadDir();
|
return true;
|
||||||
await fs.writeFile(
|
};
|
||||||
NOTEPAD_STATE_FILE,
|
|
||||||
JSON.stringify(state, null, 2),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
}, 'Failed to save notepad state');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadNotepadState() {
|
export const loadNotepadState = async () => {
|
||||||
const result = await safeExecute(async () => {
|
const stored = get('notepad') || DEFAULT_NOTEPAD_STATE;
|
||||||
|
const tabs = await getTabsFromStorage();
|
||||||
|
const activeTabId =
|
||||||
|
stored.activeTabId && tabs.some((tab) => tab.title === stored.activeTabId)
|
||||||
|
? stored.activeTabId
|
||||||
|
: tabs[0]?.title || null;
|
||||||
|
|
||||||
|
return { ...stored, tabs, activeTabId };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTabFile = async (title: string) => {
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(NOTEPAD_STATE_FILE, 'utf8');
|
await unlink(join(getNotepadDir(), `${title}.txt`));
|
||||||
return JSON.parse(data) as SavedNotepadState;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return DEFAULT_NOTEPAD_STATE;
|
return false;
|
||||||
}
|
}
|
||||||
}, 'Failed to load notepad state');
|
};
|
||||||
|
|
||||||
return result || DEFAULT_NOTEPAD_STATE;
|
export const createNewTab = async (title?: string) => {
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteTabFile(tabId: string) {
|
|
||||||
return tryExecute(async () => {
|
|
||||||
const filePath = join(NOTEPAD_DIR, `${tabId}.txt`);
|
|
||||||
try {
|
|
||||||
await fs.unlink(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
if ((error as { code?: string }).code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 'Failed to delete tab file');
|
|
||||||
}
|
|
||||||
|
|
||||||
let tabCounter = 1;
|
|
||||||
|
|
||||||
export async function createNewTab(title?: string) {
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
const state = await loadNotepadState();
|
const state = await loadNotepadState();
|
||||||
const noteNumbers = state.tabs
|
const noteNumbers = state.tabs
|
||||||
.map((tab: SavedNotepadTab) => {
|
.map((tab) => tab.title.match(/^Note (\d+)$/)?.[1])
|
||||||
const match = tab.title.match(/^Note (\d+)$/);
|
.filter(Boolean)
|
||||||
return match ? parseInt(match[1], 10) : 0;
|
.map(Number)
|
||||||
})
|
.sort((a, b) => a - b);
|
||||||
.filter((num: number) => num > 0)
|
|
||||||
.sort((a: number, b: number) => a - b);
|
|
||||||
|
|
||||||
tabCounter = 1;
|
let counter = 1;
|
||||||
for (const num of noteNumbers) {
|
for (const num of noteNumbers) {
|
||||||
if (num === tabCounter) {
|
if (num === counter) counter++;
|
||||||
tabCounter++;
|
else break;
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
title = `Note ${counter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
await saveTabContent(title, DEFAULT_TAB_CONTENT);
|
||||||
id: `tab-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
return { title, content: DEFAULT_TAB_CONTENT };
|
||||||
title: title || `Note ${tabCounter++}`,
|
|
||||||
content: DEFAULT_TAB_CONTENT,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await saveTabContent(newTab.id, newTab.content);
|
|
||||||
|
|
||||||
return newTab;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -191,13 +191,15 @@ const updaterAPI: UpdaterAPI = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const notepadAPI: NotepadAPI = {
|
const notepadAPI: NotepadAPI = {
|
||||||
saveTabContent: (tabId, content) =>
|
saveTabContent: (title, content) =>
|
||||||
ipcRenderer.invoke('notepad:saveTabContent', tabId, content),
|
ipcRenderer.invoke('notepad:saveTabContent', title, content),
|
||||||
loadTabContent: (tabId) =>
|
loadTabContent: (title) =>
|
||||||
ipcRenderer.invoke('notepad:loadTabContent', tabId),
|
ipcRenderer.invoke('notepad:loadTabContent', title),
|
||||||
|
renameTab: (oldTitle, newTitle) =>
|
||||||
|
ipcRenderer.invoke('notepad:renameTab', oldTitle, newTitle),
|
||||||
saveState: (state) => ipcRenderer.invoke('notepad:saveState', state),
|
saveState: (state) => ipcRenderer.invoke('notepad:saveState', state),
|
||||||
loadState: () => ipcRenderer.invoke('notepad:loadState'),
|
loadState: () => ipcRenderer.invoke('notepad:loadState'),
|
||||||
deleteTab: (tabId) => ipcRenderer.invoke('notepad:deleteTab', tabId),
|
deleteTab: (title) => ipcRenderer.invoke('notepad:deleteTab', title),
|
||||||
createNewTab: (title) => ipcRenderer.invoke('notepad:createNewTab', title),
|
createNewTab: (title) => ipcRenderer.invoke('notepad:createNewTab', title),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,17 @@ import {
|
||||||
interface NotepadStore extends NotepadState {
|
interface NotepadStore extends NotepadState {
|
||||||
isLoaded: boolean;
|
isLoaded: boolean;
|
||||||
setTabs: (tabs: NotepadTab[]) => void;
|
setTabs: (tabs: NotepadTab[]) => void;
|
||||||
setActiveTab: (tabId: string | null) => void;
|
setActiveTab: (title: string | null) => void;
|
||||||
addTab: (tab: NotepadTab) => void;
|
addTab: (tab: NotepadTab) => void;
|
||||||
updateTab: (tabId: string, updates: Partial<NotepadTab>) => void;
|
updateTab: (title: string, updates: Partial<NotepadTab>) => void;
|
||||||
removeTab: (tabId: string) => void;
|
removeTab: (title: string) => void;
|
||||||
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;
|
setShowLineNumbers: (showLineNumbers: boolean) => void;
|
||||||
loadState: () => Promise<void>;
|
loadState: () => Promise<void>;
|
||||||
saveState: () => Promise<void>;
|
saveState: () => Promise<void>;
|
||||||
saveTabContent: (tabId: string, content: string) => Promise<void>;
|
saveTabContent: (title: string, content: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotepadStore = create<NotepadStore>()(
|
export const useNotepadStore = create<NotepadStore>()(
|
||||||
|
|
@ -33,45 +33,55 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
|
|
||||||
setTabs: (tabs) => set({ tabs }),
|
setTabs: (tabs) => set({ tabs }),
|
||||||
|
|
||||||
setActiveTab: (tabId) => set({ activeTabId: tabId }),
|
setActiveTab: (title) => set({ activeTabId: title }),
|
||||||
|
|
||||||
addTab: (tab) => {
|
addTab: (tab) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
tabs: [...state.tabs, tab],
|
tabs: [...state.tabs, tab],
|
||||||
activeTabId: tab.id,
|
activeTabId: tab.title,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTab: (tabId, updates) => {
|
updateTab: (title, updates) => {
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
tabs: state.tabs.map((tab) =>
|
const updatedTabs = state.tabs.map((tab) =>
|
||||||
tab.id === tabId ? { ...tab, ...updates } : tab
|
tab.title === title ? { ...tab, ...updates } : tab
|
||||||
),
|
);
|
||||||
}));
|
|
||||||
|
let newActiveTabId = state.activeTabId;
|
||||||
|
if (updates.title && state.activeTabId === title) {
|
||||||
|
newActiveTabId = updates.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs: updatedTabs,
|
||||||
|
activeTabId: newActiveTabId,
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeTab: (tabId) => {
|
removeTab: (title) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
|
|
||||||
if (state.tabs.length <= 1) {
|
if (state.tabs.length <= 1) {
|
||||||
const tab = state.tabs.find((t) => t.id === tabId);
|
const tab = state.tabs.find((t) => t.title === title);
|
||||||
if (tab) {
|
if (tab) {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
tabs: state.tabs.map((t) =>
|
tabs: state.tabs.map((t) =>
|
||||||
t.id === tabId ? { ...t, content: DEFAULT_TAB_CONTENT } : t
|
t.title === title ? { ...t, content: DEFAULT_TAB_CONTENT } : t
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
window.electronAPI.notepad.saveTabContent(tabId, DEFAULT_TAB_CONTENT);
|
window.electronAPI.notepad.saveTabContent(title, DEFAULT_TAB_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTabs = state.tabs.filter((tab) => tab.id !== tabId);
|
const newTabs = state.tabs.filter((tab) => tab.title !== title);
|
||||||
const newActiveTabId =
|
const newActiveTabId =
|
||||||
state.activeTabId === tabId
|
state.activeTabId === title
|
||||||
? newTabs.length > 0
|
? newTabs.length > 0
|
||||||
? newTabs[Math.max(0, newTabs.length - 1)].id
|
? newTabs[Math.max(0, newTabs.length - 1)].title
|
||||||
: null
|
: null
|
||||||
: state.activeTabId;
|
: state.activeTabId;
|
||||||
|
|
||||||
|
|
@ -80,7 +90,7 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
activeTabId: newActiveTabId,
|
activeTabId: newActiveTabId,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.notepad.deleteTab(tabId);
|
window.electronAPI.notepad.deleteTab(title);
|
||||||
},
|
},
|
||||||
|
|
||||||
reorderTabs: (fromIndex, toIndex) => {
|
reorderTabs: (fromIndex, toIndex) => {
|
||||||
|
|
@ -109,10 +119,14 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
const savedState = await window.electronAPI.notepad.loadState();
|
const savedState = await window.electronAPI.notepad.loadState();
|
||||||
|
|
||||||
const tabsWithContent = await Promise.all(
|
const tabsWithContent = await Promise.all(
|
||||||
savedState.tabs.map(async (tab) => ({
|
savedState.tabs.map((tab) =>
|
||||||
|
window.electronAPI.notepad
|
||||||
|
.loadTabContent(tab.title)
|
||||||
|
.then((content) => ({
|
||||||
...tab,
|
...tab,
|
||||||
content: await window.electronAPI.notepad.loadTabContent(tab.id),
|
content,
|
||||||
}))
|
}))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tabsWithContent.length === 0) {
|
if (tabsWithContent.length === 0) {
|
||||||
|
|
@ -122,7 +136,8 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
|
|
||||||
set({
|
set({
|
||||||
tabs: tabsWithContent,
|
tabs: tabsWithContent,
|
||||||
activeTabId: savedState.activeTabId || tabsWithContent[0]?.id || null,
|
activeTabId:
|
||||||
|
savedState.activeTabId || tabsWithContent[0]?.title || null,
|
||||||
position: savedState.position,
|
position: savedState.position,
|
||||||
isVisible: savedState.isVisible,
|
isVisible: savedState.isVisible,
|
||||||
showLineNumbers: savedState.showLineNumbers ?? true,
|
showLineNumbers: savedState.showLineNumbers ?? true,
|
||||||
|
|
@ -132,7 +147,7 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
const defaultTab = await window.electronAPI.notepad.createNewTab();
|
const defaultTab = await window.electronAPI.notepad.createNewTab();
|
||||||
set({
|
set({
|
||||||
tabs: [defaultTab],
|
tabs: [defaultTab],
|
||||||
activeTabId: defaultTab.id,
|
activeTabId: defaultTab.title,
|
||||||
position: DEFAULT_NOTEPAD_POSITION,
|
position: DEFAULT_NOTEPAD_POSITION,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
|
|
@ -145,10 +160,6 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
if (!state.isLoaded) return;
|
if (!state.isLoaded) return;
|
||||||
|
|
||||||
await window.electronAPI.notepad.saveState({
|
await window.electronAPI.notepad.saveState({
|
||||||
tabs: state.tabs.map((tab) => ({
|
|
||||||
id: tab.id,
|
|
||||||
title: tab.title,
|
|
||||||
})),
|
|
||||||
activeTabId: state.activeTabId,
|
activeTabId: state.activeTabId,
|
||||||
position: state.position,
|
position: state.position,
|
||||||
isVisible: state.isVisible,
|
isVisible: state.isVisible,
|
||||||
|
|
@ -156,9 +167,9 @@ export const useNotepadStore = create<NotepadStore>()(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveTabContent: async (tabId, content) => {
|
saveTabContent: async (title, content) => {
|
||||||
await window.electronAPI.notepad.saveTabContent(tabId, content);
|
await window.electronAPI.notepad.saveTabContent(title, content);
|
||||||
get().updateTab(tabId, { content });
|
get().updateTab(title, { content });
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
16
src/types/electron.d.ts
vendored
16
src/types/electron.d.ts
vendored
|
|
@ -210,13 +210,11 @@ export interface UpdaterAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotepadTab {
|
export interface NotepadTab {
|
||||||
id: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedNotepadTab {
|
export interface SavedNotepadTab {
|
||||||
id: string;
|
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +230,6 @@ export interface NotepadState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SavedNotepadState {
|
export interface SavedNotepadState {
|
||||||
tabs: SavedNotepadTab[];
|
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
position: {
|
position: {
|
||||||
width: number;
|
width: number;
|
||||||
|
|
@ -242,12 +239,17 @@ export interface SavedNotepadState {
|
||||||
showLineNumbers?: boolean;
|
showLineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotepadStateWithTabs extends SavedNotepadState {
|
||||||
|
tabs: SavedNotepadTab[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotepadAPI {
|
export interface NotepadAPI {
|
||||||
saveTabContent: (tabId: string, content: string) => Promise<boolean>;
|
saveTabContent: (title: string, content: string) => Promise<boolean>;
|
||||||
loadTabContent: (tabId: string) => Promise<string>;
|
loadTabContent: (title: string) => Promise<string>;
|
||||||
|
renameTab: (oldTitle: string, newTitle: string) => Promise<boolean>;
|
||||||
saveState: (state: SavedNotepadState) => Promise<boolean>;
|
saveState: (state: SavedNotepadState) => Promise<boolean>;
|
||||||
loadState: () => Promise<SavedNotepadState>;
|
loadState: () => Promise<NotepadStateWithTabs>;
|
||||||
deleteTab: (tabId: string) => Promise<boolean>;
|
deleteTab: (title: string) => Promise<boolean>;
|
||||||
createNewTab: (title?: string) => Promise<NotepadTab>;
|
createNewTab: (title?: string) => Promise<NotepadTab>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue