191 lines
6.3 KiB
TypeScript
191 lines
6.3 KiB
TypeScript
import { DATA_DIRECTORY_URI_KEY } from '@/constants/Settings';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { format } from 'date-fns';
|
|
import * as FileSystem from 'expo-file-system';
|
|
import { StorageAccessFramework } from 'expo-file-system';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
interface FileEntry {
|
|
name: string;
|
|
uri: string;
|
|
datetime: Date;
|
|
size: number;
|
|
}
|
|
interface PrettyNameOptions {
|
|
pathContext?: 'full' | 'short';
|
|
}
|
|
interface FileInfoWithSize {
|
|
uri: string;
|
|
exists: boolean;
|
|
isDirectory: boolean | null;
|
|
modificationTime: number | null;
|
|
size: number;
|
|
}
|
|
|
|
function datetimeFromFilename(filename: string): Date {
|
|
const match = filename.match(/(\d+)-(\d+)-(\d+)-(\d+)-(\d+)-(\d+)\.md$/);
|
|
if (match) {
|
|
const [_, year, month, day, hour, minute, second] = match;
|
|
return new Date(
|
|
parseInt(year),
|
|
parseInt(month) - 1,
|
|
parseInt(day),
|
|
parseInt(hour),
|
|
parseInt(minute),
|
|
parseInt(second)
|
|
);
|
|
}
|
|
return new Date();
|
|
}
|
|
|
|
export function prettyName(uri: string | null, options: PrettyNameOptions = {}) {
|
|
if (!uri) return null;
|
|
const { pathContext = 'short' } = options;
|
|
|
|
var returnString = "";
|
|
if (pathContext === 'full') {
|
|
returnString = "/";
|
|
}
|
|
|
|
const decodedUri = decodeURIComponent(uri);
|
|
const match = decodedUri.match(/.*\/primary:(.+)/);
|
|
|
|
if (match) {
|
|
if (pathContext === 'short') {
|
|
const filename = match[1].split('/').pop() || "Unknown";
|
|
returnString = returnString.concat(filename);
|
|
}
|
|
else {
|
|
returnString = returnString.concat(match[1]);
|
|
}
|
|
}
|
|
else {
|
|
// Fallback to your original logic
|
|
returnString = returnString.concat(uri.split('%3A').pop() || "Unknown");
|
|
}
|
|
return returnString;
|
|
}
|
|
|
|
export function useFileSystem() {
|
|
const [dataDirectoryUri, setDataDirectoryUri] = useState<string | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [files, setFiles] = useState<FileEntry[]>([]);
|
|
const [directorySize, setDirectorySize] = useState<number>(0);
|
|
|
|
// Load saved directory URI
|
|
useEffect(() => {
|
|
const loadDirectoryUri = async () => {
|
|
try {
|
|
const savedUri = await AsyncStorage.getItem(DATA_DIRECTORY_URI_KEY);
|
|
if (savedUri) {
|
|
setDataDirectoryUri(savedUri);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading directory URI:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadDirectoryUri();
|
|
}, []);
|
|
|
|
// Load files when directory changes
|
|
const loadFiles = useCallback(async () => {
|
|
if (!dataDirectoryUri) return;
|
|
|
|
|
|
try {
|
|
const dirContents = await StorageAccessFramework.readDirectoryAsync(dataDirectoryUri);
|
|
const markdownFiles = [];
|
|
let totalSize = 0;
|
|
|
|
for (const fileName of dirContents.filter(name => name.endsWith('.md'))) {
|
|
try {
|
|
const fileInfo = await FileSystem.getInfoAsync(fileName) as FileInfoWithSize;
|
|
const fileEntry = {
|
|
name: prettyName(fileName) ?? "",
|
|
uri: fileInfo.uri,
|
|
datetime: datetimeFromFilename(fileName),
|
|
size: fileInfo.size,
|
|
};
|
|
|
|
markdownFiles.push(fileEntry);
|
|
totalSize += fileEntry.size;
|
|
} catch (error) {
|
|
console.warn(`Error getting info for file ${fileName}:`, error);
|
|
}
|
|
}
|
|
markdownFiles.sort((a, b) => b.datetime.getTime() - a.datetime.getTime()); // Sort by datetime descending
|
|
setFiles(markdownFiles);
|
|
setDirectorySize(Math.round((totalSize / 1024 / 1024) * 1000) / 1000); // Convert to MB
|
|
} catch (error) {
|
|
console.error('Error loading files:', error);
|
|
}
|
|
}, [dataDirectoryUri]);
|
|
|
|
useEffect(() => {
|
|
loadFiles();
|
|
}, [loadFiles]);
|
|
|
|
const writeFile = useCallback(async (fileUriOrDatetime: string, content: string) => {
|
|
if (!dataDirectoryUri) throw new Error('No directory selected');
|
|
const fileUriDatetime = new Date(parseInt(fileUriOrDatetime) * 1000);
|
|
let fileUri = fileUriOrDatetime;
|
|
if (parseInt(fileUriOrDatetime)) { // If it's a datetime in seconds
|
|
const filename = format(fileUriDatetime, 'yyyy-MM-dd-HH-mm-ss');
|
|
fileUri = await StorageAccessFramework.createFileAsync(
|
|
dataDirectoryUri,
|
|
`${filename}.md`,
|
|
'text/markdown'
|
|
);
|
|
}
|
|
// const fileUri = `${dataDirectoryUri}/${filename}`;
|
|
await FileSystem.writeAsStringAsync(fileUri, content);
|
|
await loadFiles(); // Refresh file list
|
|
}, [dataDirectoryUri, loadFiles]);
|
|
|
|
const readFile = useCallback(async (filename: string): Promise<string> => {
|
|
if (!dataDirectoryUri) throw new Error('No directory selected');
|
|
|
|
const fileUri = `${dataDirectoryUri}/${filename}`;
|
|
return await FileSystem.readAsStringAsync(fileUri);
|
|
}, [dataDirectoryUri]);
|
|
|
|
const deleteFile = useCallback(async (filename: string): Promise<void> => {
|
|
return await FileSystem.deleteAsync(filename, { idempotent: true });
|
|
}
|
|
, []);
|
|
|
|
const saveDataDirectoryUri = useCallback(async (uri: string | null) => {
|
|
try {
|
|
if (uri) {
|
|
await AsyncStorage.setItem(DATA_DIRECTORY_URI_KEY, uri);
|
|
} else {
|
|
await AsyncStorage.removeItem(DATA_DIRECTORY_URI_KEY);
|
|
}
|
|
setDataDirectoryUri(uri);
|
|
// Refresh files when directory changes
|
|
if (uri) {
|
|
await loadFiles();
|
|
} else {
|
|
setFiles([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating directory URI:', error);
|
|
}
|
|
}, [loadFiles]);
|
|
|
|
return {
|
|
dataDirectoryUri,
|
|
saveDataDirectoryUri,
|
|
isLoading,
|
|
files,
|
|
writeFile,
|
|
readFile,
|
|
deleteFile,
|
|
loadFiles,
|
|
hasDirectory: !!dataDirectoryUri,
|
|
directorySize,
|
|
};
|
|
} |