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