Insole-production time tracker exported from the Create/Anything AI platform. Baseline snapshot before any reverse-engineering or cleanup. - apps/mobile: Expo Router app (iOS/Android/web), the only workspace - publisher/: standalone OpenNext/AWS deploy tooling for the web side - Backend (/api/tasks, /api/logs + DB) lives remotely, not in this repo
204 lines
5.0 KiB
TypeScript
204 lines
5.0 KiB
TypeScript
export enum MediaTypeOptions {
|
|
All = 'All',
|
|
Images = 'Images',
|
|
Videos = 'Videos',
|
|
}
|
|
|
|
export enum UIImagePickerPresentationStyle {
|
|
FULL_SCREEN = 0,
|
|
PAGE_SHEET = 1,
|
|
FORM_SHEET = 2,
|
|
CURRENT_CONTEXT = 3,
|
|
OVERFUL_SCREEN = 4,
|
|
POPOVER = 5,
|
|
AUTOMATIC = -2,
|
|
}
|
|
|
|
interface ImagePickerAsset {
|
|
uri: string;
|
|
width: number;
|
|
height: number;
|
|
type: 'image' | 'video' | undefined;
|
|
fileName: string | null;
|
|
fileSize: number | undefined;
|
|
mimeType: string | undefined;
|
|
}
|
|
|
|
interface ImagePickerResult {
|
|
canceled: boolean;
|
|
assets: ImagePickerAsset[];
|
|
}
|
|
|
|
interface ImagePickerOptions {
|
|
mediaTypes?: MediaTypeOptions;
|
|
allowsEditing?: boolean;
|
|
quality?: number;
|
|
allowsMultipleSelection?: boolean;
|
|
base64?: boolean;
|
|
}
|
|
|
|
function getAcceptString(mediaTypes?: MediaTypeOptions): string {
|
|
switch (mediaTypes) {
|
|
case MediaTypeOptions.Images:
|
|
return 'image/*';
|
|
case MediaTypeOptions.Videos:
|
|
return 'video/*';
|
|
default:
|
|
return 'image/*,video/*';
|
|
}
|
|
}
|
|
|
|
function pickFileViaInput(
|
|
accept: string,
|
|
capture: boolean,
|
|
multiple: boolean
|
|
): Promise<ImagePickerResult> {
|
|
return new Promise((resolve) => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = accept;
|
|
input.multiple = multiple;
|
|
if (capture) input.setAttribute('capture', 'environment');
|
|
input.style.display = 'none';
|
|
document.body.appendChild(input);
|
|
|
|
let resolved = false;
|
|
const cleanup = () => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
document.body.removeChild(input);
|
|
}
|
|
};
|
|
|
|
input.addEventListener('change', () => {
|
|
const files = input.files;
|
|
if (!files || files.length === 0) {
|
|
cleanup();
|
|
resolve({ canceled: true, assets: [] });
|
|
return;
|
|
}
|
|
const promises = Array.from(files).map(
|
|
(file) =>
|
|
new Promise<ImagePickerAsset>((resolveAsset) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const uri = reader.result as string;
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
resolveAsset({
|
|
uri,
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight,
|
|
type: file.type.startsWith('video') ? 'video' : 'image',
|
|
fileName: file.name,
|
|
fileSize: file.size,
|
|
mimeType: file.type || undefined,
|
|
});
|
|
};
|
|
img.onerror = () => {
|
|
resolveAsset({
|
|
uri,
|
|
width: 0,
|
|
height: 0,
|
|
type: file.type.startsWith('video') ? 'video' : 'image',
|
|
fileName: file.name,
|
|
fileSize: file.size,
|
|
mimeType: file.type || undefined,
|
|
});
|
|
};
|
|
img.src = uri;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
})
|
|
);
|
|
void Promise.all(promises).then((assets) => {
|
|
cleanup();
|
|
resolve({ canceled: false, assets });
|
|
});
|
|
});
|
|
|
|
// Handle cancel (user closes the file dialog without selecting)
|
|
window.addEventListener(
|
|
'focus',
|
|
() => {
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
cleanup();
|
|
resolve({ canceled: true, assets: [] });
|
|
}
|
|
}, 300);
|
|
},
|
|
{ once: true }
|
|
);
|
|
|
|
input.click();
|
|
});
|
|
}
|
|
|
|
export async function launchImageLibraryAsync(
|
|
options?: ImagePickerOptions
|
|
): Promise<ImagePickerResult> {
|
|
return pickFileViaInput(
|
|
getAcceptString(options?.mediaTypes),
|
|
false,
|
|
options?.allowsMultipleSelection ?? false
|
|
);
|
|
}
|
|
|
|
export async function launchCameraAsync(
|
|
options?: ImagePickerOptions
|
|
): Promise<ImagePickerResult> {
|
|
return pickFileViaInput(
|
|
getAcceptString(options?.mediaTypes),
|
|
true,
|
|
options?.allowsMultipleSelection ?? false
|
|
);
|
|
}
|
|
|
|
const grantedPermission = {
|
|
status: 'granted' as const,
|
|
granted: true,
|
|
canAskAgain: true,
|
|
expires: 'never' as const,
|
|
};
|
|
|
|
export async function requestMediaLibraryPermissionsAsync() {
|
|
return grantedPermission;
|
|
}
|
|
|
|
export async function getMediaLibraryPermissionsAsync() {
|
|
return grantedPermission;
|
|
}
|
|
|
|
export async function requestCameraPermissionsAsync() {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
stream.getTracks().forEach((t) => t.stop());
|
|
return grantedPermission;
|
|
} catch {
|
|
return {
|
|
status: 'denied' as const,
|
|
granted: false,
|
|
canAskAgain: true,
|
|
expires: 'never' as const,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function getCameraPermissionsAsync() {
|
|
try {
|
|
const result = await navigator.permissions.query({
|
|
name: 'camera' as PermissionName,
|
|
});
|
|
const granted = result.state === 'granted';
|
|
return {
|
|
status: granted ? ('granted' as const) : ('denied' as const),
|
|
granted,
|
|
canAskAgain: result.state !== 'denied',
|
|
expires: 'never' as const,
|
|
};
|
|
} catch {
|
|
return grantedPermission;
|
|
}
|
|
}
|