Skip to content

Commit 83d535f

Browse files
committed
Add file picker for web and mobile
1 parent 3318084 commit 83d535f

File tree

14 files changed

+176
-89
lines changed

14 files changed

+176
-89
lines changed

android/app/capacitor.build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ android {
99

1010
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
1111
dependencies {
12+
implementation project(':capacitor-filesystem')
1213
implementation project(':capacitor-screen-orientation')
1314
implementation project(':capacitor-status-bar')
1415

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@
3737

3838
<!-- Permissions -->
3939

40+
<!-- Internet -->
4041
<uses-permission android:name="android.permission.INTERNET" />
4142
<!-- Request Microphone -->
4243
<uses-permission android:name="android.permission.RECORD_AUDIO" />
43-
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
44+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
45+
<!-- Storage -->
46+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
47+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
4448
</manifest>

android/capacitor.settings.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
include ':capacitor-android'
33
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
44

5+
include ':capacitor-filesystem'
6+
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
7+
58
include ':capacitor-screen-orientation'
69
project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android')
710

components/nav-menu.tsx

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Button } from "@nextui-org/react";
22
import { AnimatePresence, motion } from "framer-motion";
33
import { useMediaQuery } from "react-responsive";
44
import { useFileSystem } from "@/lib/hooks/use-file-system";
5-
import { CodeEditorViewRef } from "./views/code-editor-view";
65
import { EditorContext } from "./providers/editor-context-provider";
76
import { useContext } from "react";
87
import { ViewTypeEnum } from "@/lib/views/available-views";
@@ -58,8 +57,14 @@ function MenuPanel({ children }: { children?: React.ReactNode }) {
5857
);
5958
}
6059

61-
export default function NavMenu({ isMenuOpen }: { isMenuOpen: boolean }) {
62-
const { projectPath, openFilePicker, readFile } = useFileSystem();
60+
export default function NavMenu({
61+
isMenuOpen,
62+
setIsMenuOpen,
63+
}: {
64+
isMenuOpen: boolean;
65+
setIsMenuOpen: (isOpen: boolean) => void;
66+
}) {
67+
const { projectPath, openFile } = useFileSystem();
6368

6469
const editorContext = useContext(EditorContext);
6570

@@ -72,48 +77,39 @@ export default function NavMenu({ isMenuOpen }: { isMenuOpen: boolean }) {
7277
{!projectPath && (
7378
<div className="flex w-full flex-wrap justify-center gap-x-1 gap-y-1">
7479
<Button className="w-40">New Project</Button>
75-
<Button
76-
className="w-40"
77-
onPress={() => {
78-
openFilePicker(true).then((folderPaths) => {
79-
console.log(folderPaths);
80-
});
81-
}}
82-
>
80+
<Button className="w-40" onPress={() => {}}>
8381
Open Project
8482
</Button>
8583
<Button className="w-40">Save Project</Button>
8684
<Button className="w-40">New File</Button>
8785
<Button
8886
className="w-40"
8987
onPress={() => {
90-
openFilePicker(false).then((filePaths) => {
91-
console.log(filePaths);
92-
const filePath = filePaths[0];
93-
// Open the first file
94-
readFile(filePath).then((file) => {
95-
console.log(file);
96-
file.text().then((text) => {
97-
console.log("File content:\n" + text);
98-
const viewDocument: ViewDocument = {
99-
fileContent: text,
100-
filePath: filePath,
101-
};
102-
const view = new View(
103-
ViewTypeEnum.Code,
104-
viewDocument,
105-
);
88+
openFile().then((file) => {
89+
console.log(file);
90+
file?.text().then((text) => {
91+
console.log("File content:\n" + text);
92+
const viewDocument: ViewDocument = {
93+
fileContent: text,
94+
filePath: file.name,
95+
};
96+
const view = new View(
97+
ViewTypeEnum.Code,
98+
viewDocument,
99+
);
106100

107-
// Notify state update
108-
editorContext?.setViewManager((prev) => {
109-
const newVM = ViewManager.copy(prev);
110-
// Add view to view manager
111-
newVM?.addView(view);
112-
// Set the view as active
113-
newVM?.setActiveView(view);
114-
return newVM;
115-
});
101+
// Notify state update
102+
editorContext?.setViewManager((prev) => {
103+
const newVM = ViewManager.copy(prev);
104+
newVM?.clearView();
105+
// Add view to view manager
106+
newVM?.addView(view);
107+
// Set the view as active
108+
newVM?.setActiveView(view);
109+
return newVM;
116110
});
111+
112+
setIsMenuOpen(false);
117113
});
118114
});
119115
}}

components/nav.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ export default function Nav({ children }: { children: React.ReactNode }) {
155155
<div
156156
className={`flex h-full w-full overflow-hidden ${isShowNavbar ? "pt-[48px]" : ""}`}
157157
>
158-
{isShowNavbar && <NavMenu isMenuOpen={isMenuOpen} />}
158+
{isShowNavbar && (
159+
<NavMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
160+
)}
159161

160162
<div className="min-w-0 flex-grow">{children}</div>
161163
</div>

components/view-display-area.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { View } from "@/lib/views/view";
1212

1313
export default function ViewDisplayArea() {
1414
const editorContext = useContext(EditorContext);
15+
const [activeView, setActiveView] = useState<View | undefined>(undefined);
1516

1617
// Initialize view manager
1718
useEffect(() => {
@@ -28,6 +29,14 @@ export default function ViewDisplayArea() {
2829
}
2930
}, []);
3031

32+
useEffect(() => {
33+
if (editorContext?.viewManager) {
34+
const activeView = editorContext.viewManager.getActiveView();
35+
console.log("Active view:", activeView?.viewDocument.filePath);
36+
setActiveView(activeView);
37+
}
38+
}, [editorContext?.viewManager]);
39+
3140
function notifyVSCode() {
3241
window.parent.postMessage(
3342
{
@@ -105,7 +114,7 @@ export default function ViewDisplayArea() {
105114
<div className="flex h-full w-full flex-col p-1">
106115
<div className="flex h-full w-full flex-col items-start justify-between gap-1.5 overflow-hidden rounded-xl bg-default p-2">
107116
<div className={`min-h-0 w-full flex-grow`}>
108-
{editorContext?.viewManager?.viewCount() === 0 ? (
117+
{!activeView ? (
109118
<div className="flex h-full w-full flex-col items-center justify-center gap-y-1 pb-12 text-default-foreground">
110119
<h1 className="text-center text-2xl font-bold">
111120
Welcome to Chisel Editor!
@@ -115,19 +124,15 @@ export default function ViewDisplayArea() {
115124
</p>
116125
</div>
117126
) : (
118-
editorContext?.viewManager
119-
?.getViewByType(ViewTypeEnum.Code)
120-
.map((view, index) => (
121-
<CodeEditorView
122-
key={index}
123-
ref={(ref) => {
124-
if (ref) view.viewRef = ref;
125-
}}
126-
width="100%"
127-
height="100%"
128-
view={view}
129-
/>
130-
))
127+
<CodeEditorView
128+
key={activeView.viewDocument.filePath}
129+
ref={(ref) => {
130+
if (ref) activeView.viewRef = ref;
131+
}}
132+
width="100%"
133+
height="100%"
134+
view={activeView}
135+
/>
131136
)}
132137
</div>
133138

lib/hooks/use-file-system.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,12 @@ export function useFileSystem() {
3030
}
3131
}, []);
3232

33-
async function openFilePicker(isFolder: boolean): Promise<string[]> {
33+
async function openFolder(): Promise<Folder | undefined> {
3434
if (platformApi.current === undefined) {
3535
throw new Error("Platform API not initialized");
3636
}
3737

38-
return await platformApi.current.openFilePicker(isFolder);
39-
}
40-
41-
async function openFolder(uri: string): Promise<Folder> {
42-
if (platformApi.current === undefined) {
43-
throw new Error("Platform API not initialized");
44-
}
45-
46-
return await platformApi.current.openFolder(uri);
38+
return await platformApi.current.openFolder();
4739
}
4840

4941
async function saveFolder(folder: Folder, uriPrefix: string) {
@@ -54,12 +46,12 @@ export function useFileSystem() {
5446
platformApi.current.saveFolder(folder, uriPrefix);
5547
}
5648

57-
async function readFile(uri: string): Promise<File> {
49+
async function openFile(): Promise<File | undefined> {
5850
if (platformApi.current === undefined) {
5951
throw new Error("Platform API not initialized");
6052
}
6153

62-
return platformApi.current.readFile(uri);
54+
return platformApi.current.openFile();
6355
}
6456

6557
async function writeFile(file: File, uri: string) {
@@ -72,10 +64,9 @@ export function useFileSystem() {
7264

7365
return {
7466
projectPath,
75-
openFilePicker,
7667
openFolder,
7768
saveFolder,
78-
readFile,
69+
openFile,
7970
writeFile,
8071
};
8172
}
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Folder } from "../types";
22

33
export abstract class AbstractPlatformAPI {
4-
abstract openFilePicker(isFolder: boolean): Promise<string[]>;
5-
abstract openFolder(uri: string): Promise<Folder>;
4+
abstract openFolder(): Promise<Folder | undefined>;
65
abstract saveFolder(folder: Folder, uriPrefix: string): Promise<void>;
7-
abstract readFile(uri: string): Promise<File>;
6+
abstract openFile(): Promise<File | undefined>;
87
abstract writeFile(file: File, uri: string): Promise<void>;
98
}
Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,53 @@
11
import { Folder } from "@/lib/types";
22
import { AbstractPlatformAPI } from "../abstract-platform-api";
3+
import { Filesystem } from "@capacitor/filesystem";
34

45
export class CapacitorAPI extends AbstractPlatformAPI {
5-
async openFilePicker(isFolder: boolean): Promise<string[]> {
6-
throw new Error("Method not implemented.");
6+
constructor() {
7+
super();
78
}
8-
async openFolder(uri: string): Promise<Folder> {
9+
10+
async openFolder(): Promise<Folder | undefined> {
911
throw new Error("Method not implemented.");
1012
}
1113
async saveFolder(folder: Folder, uriPrefix: string): Promise<void> {
1214
throw new Error("Method not implemented.");
1315
}
14-
async readFile(uri: string): Promise<File> {
15-
throw new Error("Method not implemented.");
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];
1622
}
1723
async writeFile(file: File, uri: string): Promise<void> {
1824
throw new Error("Method not implemented.");
1925
}
26+
27+
private async openFilePicker(): Promise<File[]> {
28+
const hasPermission = await Filesystem.requestPermissions();
29+
if (hasPermission.publicStorage !== "granted") {
30+
return [];
31+
}
32+
return new Promise((resolve, reject) => {
33+
const fileInput = document.createElement("input");
34+
fileInput.type = "file";
35+
fileInput.style.display = "none";
36+
fileInput.multiple = true;
37+
38+
// Update paths when files are selected
39+
fileInput.addEventListener("change", () => {
40+
const fileList = fileInput.files;
41+
if (fileList) {
42+
const files = Array.from(fileList);
43+
resolve(files);
44+
} else {
45+
reject(new Error("No files selected"));
46+
}
47+
});
48+
49+
// Open file picker
50+
fileInput.click();
51+
});
52+
}
2053
}

lib/platform-api/electron/electron-api.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,29 @@ export class ElectronAPI extends AbstractPlatformAPI {
99
this.electronAPI = window.electronAPI;
1010
}
1111

12-
async openFilePicker(isFolder: boolean): Promise<string[]> {
13-
// Open a file dialogue and return the selected folder
14-
const paths: string[] = await this.electronAPI.openFilePicker(isFolder);
15-
return paths;
16-
}
17-
async openFolder(uri: string): Promise<Folder> {
12+
async openFolder(): Promise<Folder | undefined> {
1813
throw new Error("Method not implemented.");
1914
}
2015
async saveFolder(folder: Folder, uriPrefix: string): Promise<void> {
2116
throw new Error("Method not implemented.");
2217
}
23-
async readFile(uri: string): Promise<File> {
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];
2424
const data: string = await this.electronAPI.readFile(uri);
2525
const file = new File([data], uri);
2626
return file;
2727
}
2828
async writeFile(file: File, uri: string): Promise<void> {
2929
throw new Error("Method not implemented.");
3030
}
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;
36+
}
3137
}

0 commit comments

Comments
 (0)