diff --git a/mobile/app/_layout.jsx b/mobile/app/_layout.tsx
similarity index 100%
rename from mobile/app/_layout.jsx
rename to mobile/app/_layout.tsx
diff --git a/mobile/app/index.jsx b/mobile/app/index.tsx
similarity index 100%
rename from mobile/app/index.jsx
rename to mobile/app/index.tsx
diff --git a/mobile/context/AuthContext.jsx b/mobile/context/AuthContext.jsx
deleted file mode 100644
index e8cfcb3..0000000
--- a/mobile/context/AuthContext.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { createContext, useCallback, useContext, useMemo, useState } from 'react';
-
-const AuthContext = createContext(null);
-
-export function AuthProvider({ children }) {
-
- return {children};
-}
-
-export function useAuth() {
- const ctx = useContext(AuthContext);
- if (!ctx) {
- throw new Error('useAuth must be used within an AuthProvider');
- }
- return ctx;
-}
\ No newline at end of file
diff --git a/mobile/context/AuthContext.tsx b/mobile/context/AuthContext.tsx
new file mode 100644
index 0000000..ea8b51a
--- /dev/null
+++ b/mobile/context/AuthContext.tsx
@@ -0,0 +1,27 @@
+import { createContext, useCallback, useContext, useMemo, useState } from 'react';
+import type { ReactNode } from 'react';
+
+export type User = {
+};
+
+type AuthContextValue = {
+ user: User | null;
+};
+
+const AuthContext = createContext(null);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+
+ const value = useMemo(() => ({ user }), [user]);
+
+ return {children};
+}
+
+export function useAuth(): AuthContextValue {
+ const ctx = useContext(AuthContext);
+ if (!ctx) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return ctx;
+}
\ No newline at end of file
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index 5ac5f8f..7e9f98b 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -10,10 +10,12 @@
"dependencies": {
"expo": "~57.0.8",
"expo-constants": "~57.0.17",
+ "expo-file-system": "~57.0.6",
"expo-linking": "~57.0.9",
"expo-router": "~57.0.20",
"expo-status-bar": "~57.0.1",
"react": "19.2.3",
+ "react-dom": "19.2.3",
"react-native": "0.86.0",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0"
@@ -3495,6 +3497,16 @@
"react-native": "*"
}
},
+ "node_modules/expo-file-system": {
+ "version": "57.0.6",
+ "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz",
+ "integrity": "sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-font": {
"version": "57.0.3",
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-57.0.3.tgz",
@@ -4052,16 +4064,6 @@
"react-native": "*"
}
},
- "node_modules/expo/node_modules/expo-file-system": {
- "version": "57.0.6",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-57.0.6.tgz",
- "integrity": "sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
"node_modules/expo/node_modules/expo-keep-awake": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-57.0.1.tgz",
@@ -6049,16 +6051,15 @@
}
},
"node_modules/react-dom": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
- "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.2.8"
+ "react": "^19.2.3"
}
},
"node_modules/react-fast-compare": {
diff --git a/mobile/package.json b/mobile/package.json
index c5bb63c..85dbbd0 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -5,10 +5,12 @@
"dependencies": {
"expo": "~57.0.8",
"expo-constants": "~57.0.17",
+ "expo-file-system": "~57.0.6",
"expo-linking": "~57.0.9",
"expo-router": "~57.0.20",
"expo-status-bar": "~57.0.1",
"react": "19.2.3",
+ "react-dom": "19.2.3",
"react-native": "0.86.0",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0"
diff --git a/mobile/services/safDirectory.ts b/mobile/services/safDirectory.ts
new file mode 100644
index 0000000..5843146
--- /dev/null
+++ b/mobile/services/safDirectory.ts
@@ -0,0 +1,129 @@
+import { Directory, File, Paths } from 'expo-file-system';
+
+export type Folder = {
+ uri: string;
+ name: string;
+ isDirectory: true;
+ exists?: boolean;
+};
+
+export type FileEntry = {
+ uri: string;
+ name: string;
+ isDirectory: false;
+ extension: string;
+ exists: boolean;
+ size: number;
+ type: string;
+ lastModified: number | null;
+};
+
+export type DirectoryEntry = Folder | FileEntry;
+
+export type FolderInfo = {
+ uri: string;
+ name: string;
+ exists: boolean;
+};
+
+type PickDirectoryOptions = {
+ recursive?: boolean;
+ includeRoot?: boolean;
+};
+
+export async function pickDirectory(initialUri?: string): Promise {
+ try {
+ const directory = await Directory.pickDirectoryAsync(initialUri);
+ if (!directory || !directory.uri) return null;
+ return toFolder({ uri: directory.uri, name: directory.name, exists: directory.exists });
+ } catch {
+ return null;
+ }
+}
+
+export function listDirectory(directoryUri: string): DirectoryEntry[] {
+ const directory = new Directory(directoryUri);
+ if (!directory.exists) return [];
+ try {
+ return directory.list().map(toEntry);
+ } catch {
+ return [];
+ }
+}
+
+export function listFolders(directoryUri: string, options: PickDirectoryOptions = {}): Folder[] {
+ const { recursive = false, includeRoot = false } = options;
+ if (!recursive) {
+ return listDirectory(directoryUri).filter((entry): entry is Folder => entry.isDirectory);
+ }
+
+ const result: Folder[] = [];
+ if (includeRoot) {
+ result.push({ ...getFolderInfo(directoryUri), isDirectory: true });
+ }
+ const visit = (uri: string) => {
+ for (const entry of listDirectory(uri)) {
+ if (entry.isDirectory) {
+ result.push(entry);
+ visit(entry.uri);
+ }
+ }
+ };
+ visit(directoryUri);
+ return result;
+}
+
+export function getFolderInfo(directoryUri: string): FolderInfo {
+ const directory = new Directory(directoryUri);
+ return {
+ uri: directory.uri,
+ name: directory.name,
+ exists: directory.exists,
+ };
+}
+
+export function createDirectory(directoryUri: string, name: string): Folder {
+ const directory = new Directory(Paths.join(directoryUri, name));
+ directory.create({ idempotent: true, intermediates: true });
+ return toFolder({ uri: directory.uri, name: directory.name, exists: directory.exists });
+}
+
+export function fileFromUri(uri: string): FileEntry {
+ const file = new File(uri);
+ return {
+ uri: file.uri,
+ name: file.name,
+ isDirectory: false,
+ extension: file.extension,
+ exists: file.exists,
+ size: file.size,
+ type: file.type,
+ lastModified: file.lastModified,
+ };
+}
+
+function toEntry(item: Directory | File): DirectoryEntry {
+ if (item instanceof Directory) {
+ return toFolder({ uri: item.uri, name: item.name, exists: item.exists });
+ }
+ return {
+ uri: item.uri,
+ name: item.name,
+ isDirectory: false,
+ extension: item.extension,
+ exists: item.exists,
+ size: item.size,
+ type: item.type,
+ lastModified: item.lastModified,
+ };
+}
+
+function toFolder({ uri, name, exists }: { uri: string; name?: string; exists?: boolean }): Folder {
+ const directory = exists === undefined ? new Directory(uri) : null;
+ return {
+ uri,
+ name: name ?? directory?.name ?? Paths.basename(uri),
+ isDirectory: true,
+ exists: exists ?? directory?.exists,
+ };
+}
\ No newline at end of file