Skip to content

Commit a3915d5

Browse files
committed
Add save file dialog for desktop
1 parent c10627e commit a3915d5

File tree

9 files changed

+182
-74
lines changed

9 files changed

+182
-74
lines changed

components/nav-menu.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ViewTypeEnum } from "@/lib/views/available-views";
88
import { ViewDocument } from "@/lib/types";
99
import { View } from "@/lib/views/view";
1010
import { ViewManager } from "@/lib/views/view-manager";
11+
import toast from "react-hot-toast";
1112

1213
function MenuPanel({ children }: { children?: React.ReactNode }) {
1314
const isDesktop = useMediaQuery({
@@ -64,7 +65,13 @@ export default function NavMenu({
6465
isMenuOpen: boolean;
6566
setIsMenuOpen: (isOpen: boolean) => void;
6667
}) {
67-
const { projectPath, openFile } = useFileSystem();
68+
const {
69+
projectPath,
70+
showOpenFileDialog,
71+
showSaveFileDialog,
72+
openFile,
73+
writeFile,
74+
} = useFileSystem();
6875

6976
const editorContext = useContext(EditorContext);
7077

@@ -112,13 +119,14 @@ export default function NavMenu({
112119
<Button
113120
className="w-40"
114121
onPress={() => {
115-
openFile().then((file) => {
116-
console.log(file);
117-
file?.text().then((text) => {
122+
showOpenFileDialog().then((files) => {
123+
console.log(files);
124+
const firstFile = files[0];
125+
firstFile?.text().then((text) => {
118126
console.log("File content:\n" + text);
119127
const viewDocument: ViewDocument = {
120128
fileContent: text,
121-
filePath: file.name,
129+
filePath: firstFile.name,
122130
};
123131
openDocumentInView(viewDocument);
124132
});
@@ -127,7 +135,28 @@ export default function NavMenu({
127135
>
128136
Open File
129137
</Button>
130-
<Button className="w-40">Save File</Button>
138+
<Button
139+
className="w-40"
140+
onPress={() => {
141+
const viewDocument =
142+
editorContext?.viewManager?.getActiveView()
143+
?.viewDocument;
144+
if (viewDocument) {
145+
showSaveFileDialog().then((filePath) => {
146+
if (filePath) {
147+
writeFile(
148+
new File([viewDocument.fileContent], filePath),
149+
filePath,
150+
).then(() => {
151+
toast.success("File saved successfully");
152+
});
153+
}
154+
});
155+
}
156+
}}
157+
>
158+
Save File
159+
</Button>
131160
</div>
132161
)}
133162
</div>

desktop/main.mjs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const createWindow = () => {
2727
color: "#00000000",
2828
symbolColor: "#74b1be",
2929
},
30-
icon: path.join(__dirname, "../public/icons/electron/pulse_logo_round")
30+
icon: path.join(__dirname, "../public/icons/electron/pulse_logo_round"),
3131
});
3232

3333
win.menuBarVisible = false;
@@ -57,27 +57,42 @@ function handleSetTitle(event, title) {
5757
return title[0];
5858
}
5959

60-
async function handleOpenFilePicker(event, isFolder) {
60+
async function handleShowOpenFileDialog(event, config) {
6161
const { canceled, filePaths } = await dialog.showOpenDialog({
62-
properties: isFolder ? ["openDirectory"] : ["openFile"],
62+
properties: config?.isFolder ? ["openDirectory"] : ["openFile"],
6363
});
6464
if (!canceled) {
6565
return filePaths;
6666
}
6767
}
6868

69+
async function handleShowSaveFileDialog(event, config) {
70+
const { canceled, filePath } = await dialog.showSaveDialog({});
71+
if (!canceled) {
72+
return filePath;
73+
}
74+
return undefined;
75+
}
76+
6977
async function handleReadFile(event, path) {
7078
// Read the file at path
7179
const data = await fs.promises.readFile(path, "utf-8");
7280

7381
return data;
7482
}
7583

84+
async function handleWriteFile(event, data, path) {
85+
// Write the data at path
86+
await fs.promises.writeFile(path, data);
87+
}
88+
7689
app.whenReady().then(() => {
7790
nativeTheme.themeSource = "light";
7891

79-
ipcMain.handle("open-file-picker", handleOpenFilePicker);
92+
ipcMain.handle("show-open-file-dialog", handleShowOpenFileDialog);
93+
ipcMain.handle("show-save-file-dialog", handleShowSaveFileDialog);
8094
ipcMain.handle("read-file", handleReadFile);
95+
ipcMain.handle("write-file", handleWriteFile);
8196
createWindow();
8297
});
8398

desktop/preload.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
const { contextBridge, ipcRenderer } = require("electron");
22

33
contextBridge.exposeInMainWorld("electronAPI", {
4-
openFilePicker: (isFolder) =>
5-
ipcRenderer.invoke("open-file-picker", isFolder),
4+
showOpenFileDialog: (config) =>
5+
ipcRenderer.invoke("show-open-file-dialog", config),
6+
showSaveFileDialog: (config) =>
7+
ipcRenderer.invoke("show-save-file-dialog", config),
68
readFile: (path) => ipcRenderer.invoke("read-file", path),
9+
writeFile: (data, path) => ipcRenderer.invoke("write-file", data, path),
710
});

lib/hooks/use-file-system.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"use client";
22

33
import { useEffect, useRef, useState } from "react";
4-
import { Folder } from "@/lib/types";
4+
import {
5+
OpenFileDialogConfig,
6+
Folder,
7+
SaveFileDialogConfig,
8+
} from "@/lib/types";
59
import { getPlatform } from "../platform-api/platform-checker";
610
import { PlatformEnum } from "../platform-api/available-platforms";
711
import { CapacitorAPI } from "../platform-api/capacitor/capacitor-api";
@@ -30,12 +34,32 @@ export function useFileSystem() {
3034
}
3135
}, []);
3236

33-
async function openFolder(): Promise<Folder | undefined> {
37+
async function showOpenFileDialog(
38+
config?: OpenFileDialogConfig,
39+
): Promise<File[]> {
3440
if (platformApi.current === undefined) {
3541
throw new Error("Platform API not initialized");
3642
}
3743

38-
return await platformApi.current.openFolder();
44+
return await platformApi.current.showOpenFileDialog(config);
45+
}
46+
47+
async function showSaveFileDialog(
48+
config?: SaveFileDialogConfig,
49+
): Promise<string | undefined> {
50+
if (platformApi.current === undefined) {
51+
throw new Error("Platform API not initialized");
52+
}
53+
54+
return await platformApi.current.showSaveFileDialog(config);
55+
}
56+
57+
async function openFolder(uri: string): Promise<Folder | undefined> {
58+
if (platformApi.current === undefined) {
59+
throw new Error("Platform API not initialized");
60+
}
61+
62+
return await platformApi.current.openFolder(uri);
3963
}
4064

4165
async function saveFolder(folder: Folder, uriPrefix: string) {
@@ -46,12 +70,12 @@ export function useFileSystem() {
4670
platformApi.current.saveFolder(folder, uriPrefix);
4771
}
4872

49-
async function openFile(): Promise<File | undefined> {
73+
async function openFile(uri: string): Promise<File | undefined> {
5074
if (platformApi.current === undefined) {
5175
throw new Error("Platform API not initialized");
5276
}
5377

54-
return platformApi.current.openFile();
78+
return platformApi.current.openFile(uri);
5579
}
5680

5781
async function writeFile(file: File, uri: string) {
@@ -64,6 +88,8 @@ export function useFileSystem() {
6488

6589
return {
6690
projectPath,
91+
showOpenFileDialog,
92+
showSaveFileDialog,
6793
openFolder,
6894
saveFolder,
6995
openFile,
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { Folder } from "../types";
1+
import { OpenFileDialogConfig, Folder, SaveFileDialogConfig } from "../types";
22

33
export abstract class AbstractPlatformAPI {
4-
abstract openFolder(): Promise<Folder | undefined>;
4+
// Dialogs
5+
abstract showOpenFileDialog(config?: OpenFileDialogConfig): Promise<File[]>;
6+
abstract showSaveFileDialog(config?: SaveFileDialogConfig): Promise<string | undefined>;
7+
8+
abstract openFolder(uri: string): Promise<Folder | undefined>;
59
abstract saveFolder(folder: Folder, uriPrefix: string): Promise<void>;
6-
abstract openFile(): Promise<File | undefined>;
10+
abstract openFile(uri: string): Promise<File | undefined>;
711
abstract writeFile(file: File, uri: string): Promise<void>;
812
}
Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { Folder } from "@/lib/types";
1+
import {
2+
OpenFileDialogConfig as OpenFileDialogConfig,
3+
Folder,
4+
SaveFileDialogConfig,
5+
} from "@/lib/types";
26
import { AbstractPlatformAPI } from "../abstract-platform-api";
37
import { Filesystem } from "@capacitor/filesystem";
48

@@ -7,24 +11,7 @@ export class CapacitorAPI extends AbstractPlatformAPI {
711
super();
812
}
913

10-
async openFolder(): Promise<Folder | undefined> {
11-
throw new Error("Method not implemented.");
12-
}
13-
async saveFolder(folder: Folder, uriPrefix: string): Promise<void> {
14-
throw new Error("Method not implemented.");
15-
}
16-
async openFile(): Promise<File | undefined> {
17-
const files = await this.openFilePicker();
18-
if (files.length === 0) {
19-
return undefined;
20-
}
21-
return files[0];
22-
}
23-
async writeFile(file: File, uri: string): Promise<void> {
24-
throw new Error("Method not implemented.");
25-
}
26-
27-
private async openFilePicker(): Promise<File[]> {
14+
async showOpenFileDialog(config?: OpenFileDialogConfig): Promise<File[]> {
2815
const hasPermission = await Filesystem.requestPermissions();
2916
if (hasPermission.publicStorage !== "granted") {
3017
return [];
@@ -50,4 +37,23 @@ export class CapacitorAPI extends AbstractPlatformAPI {
5037
fileInput.click();
5138
});
5239
}
40+
41+
async showSaveFileDialog(
42+
config?: SaveFileDialogConfig,
43+
): Promise<string | undefined> {
44+
throw new Error("Method not implemented.");
45+
}
46+
47+
async openFolder(uri: string): Promise<Folder | undefined> {
48+
throw new Error("Method not implemented.");
49+
}
50+
async saveFolder(folder: Folder, uriPrefix: string): Promise<void> {
51+
throw new Error("Method not implemented.");
52+
}
53+
async openFile(uri: string): Promise<File | undefined> {
54+
throw new Error("Method not implemented.");
55+
}
56+
async writeFile(file: File, uri: string): Promise<void> {
57+
throw new Error("Method not implemented.");
58+
}
5359
}
Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Folder } from "@/lib/types";
1+
import { OpenFileDialogConfig, Folder, SaveFileDialogConfig } from "@/lib/types";
22
import { AbstractPlatformAPI } from "../abstract-platform-api";
33

44
export class ElectronAPI extends AbstractPlatformAPI {
@@ -9,29 +9,39 @@ export class ElectronAPI extends AbstractPlatformAPI {
99
this.electronAPI = window.electronAPI;
1010
}
1111

12-
async openFolder(): Promise<Folder | undefined> {
12+
async showOpenFileDialog(config?: OpenFileDialogConfig): Promise<File[]> {
13+
// Open a file dialogue and return the selected folder
14+
const paths: string[] = await this.electronAPI.showOpenFileDialog(config);
15+
16+
const files = [];
17+
for (const path of paths) {
18+
const data: string = await this.electronAPI.readFile(path);
19+
const file = new File([data], path);
20+
files.push(file);
21+
}
22+
23+
return files;
24+
}
25+
26+
async showSaveFileDialog(
27+
config?: SaveFileDialogConfig,
28+
): Promise<string | undefined> {
29+
return await this.electronAPI.showSaveFileDialog(config);
30+
}
31+
32+
async openFolder(uri: string): Promise<Folder | undefined> {
1333
throw new Error("Method not implemented.");
1434
}
1535
async saveFolder(folder: Folder, uriPrefix: string): Promise<void> {
1636
throw new Error("Method not implemented.");
1737
}
18-
async openFile(): Promise<File | undefined> {
19-
const paths = await this.openFilePicker(false);
20-
if (paths.length === 0){
21-
return undefined;
22-
}
23-
const uri = paths[0];
38+
async openFile(uri: string): Promise<File | undefined> {
2439
const data: string = await this.electronAPI.readFile(uri);
2540
const file = new File([data], uri);
2641
return file;
2742
}
2843
async writeFile(file: File, uri: string): Promise<void> {
29-
throw new Error("Method not implemented.");
30-
}
31-
32-
private async openFilePicker(isFolder: boolean): Promise<string[]> {
33-
// Open a file dialogue and return the selected folder
34-
const paths: string[] = await this.electronAPI.openFilePicker(isFolder);
35-
return paths;
44+
const data = await file.text();
45+
await this.electronAPI.writeFile(data, uri);
3646
}
3747
}

0 commit comments

Comments
 (0)