Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

275 lines
10 KiB
TypeScript

/**
* Dialog Components for FileManager
*/
import React, { useState, useEffect } from 'react';
import { FSNode } from '../../../os/types';
import * as FileSystem from '../../../os/FileSystemService';
import { getFileIcon, getFileIconColor } from '../utils/fileUtils';
import { strings } from '../res/strings';
import { stringsEn } from '../res/strings.en';
import { useAppStrings } from '@/os/useAppStrings';
import * as TimeService from '@/os/TimeService';
// ============================================================================
// Base Dialog
// ============================================================================
interface DialogProps {
open: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
export const Dialog: React.FC<DialogProps> = ({ open, onClose, title, children }) => {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-app-surface rounded-2xl w-[85%] max-w-[320px] overflow-hidden shadow-xl">
<div className="px-5 py-4 border-b border-gray-100">
<h3 className="text-[17px] font-semibold text-app-text">{title}</h3>
</div>
<div className="p-5">
{children}
</div>
</div>
</div>
);
};
// ============================================================================
// Input Dialog
// ============================================================================
interface InputDialogProps {
open: boolean;
onClose: () => void;
title: string;
placeholder?: string;
defaultValue?: string;
onConfirm: (value: string) => void;
confirmText?: string;
}
export const InputDialog: React.FC<InputDialogProps> = ({
open, onClose, title, placeholder, defaultValue = '', onConfirm, confirmText
}) => {
const s = useAppStrings(strings, stringsEn);
const effectiveConfirmText = confirmText ?? s.dialog_confirm;
const [value, setValue] = useState(defaultValue);
useEffect(() => {
if (open) setValue(defaultValue);
}, [open, defaultValue]);
const handleConfirm = () => {
if (value.trim()) {
onConfirm(value.trim());
// Note: onConfirm is responsible for closing the dialog via back()
}
};
return (
<Dialog open={open} onClose={onClose} title={title}>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder={placeholder}
className="w-full px-4 py-3 border border-app-border rounded-xl text-[15px] focus:outline-none focus:border-blue-500"
autoFocus
onKeyDown={e => e.key === 'Enter' && handleConfirm()}
/>
<div className="flex gap-3 mt-5">
<button
onClick={onClose}
className="flex-1 py-3 rounded-xl bg-gray-100 text-[15px] font-medium text-gray-700"
>
{s.dialog_cancel}
</button>
<button
onClick={handleConfirm}
disabled={!value.trim()}
className="flex-1 py-3 rounded-xl bg-blue-500 text-[15px] font-medium text-white disabled:opacity-50"
>
{effectiveConfirmText}
</button>
</div>
</Dialog>
);
};
// ============================================================================
// Confirm Dialog
// ============================================================================
interface ConfirmDialogProps {
open: boolean;
onClose: () => void;
title: string;
message: string;
onConfirm: () => void;
confirmText?: string;
cancelText?: string;
isDestructive?: boolean;
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
open, onClose, title, message, onConfirm,
confirmText, cancelText, isDestructive = false
}) => {
const s = useAppStrings(strings, stringsEn);
const effectiveCancelText = cancelText ?? s.dialog_cancel;
const effectiveConfirmText = confirmText ?? s.dialog_confirm;
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-end justify-center pb-10 sm:items-center sm:pb-0 px-4">
<div className="absolute inset-0 bg-black/40 transition-opacity" onClick={onClose} />
<div className="relative bg-app-surface rounded-[32px] w-full max-w-[340px] overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200">
<div className="px-6 pt-8 pb-4 text-center">
<h3 className="text-[20px] font-semibold text-app-text mb-4">{title}</h3>
<p className="text-[16px] text-gray-600 leading-relaxed px-2">
{message}
</p>
</div>
<div className="flex gap-3 p-6 pt-2">
<button
onClick={onClose}
className="flex-1 py-4 rounded-2xl bg-gray-50 text-[17px] font-medium text-app-text active:bg-gray-100 transition-colors"
>
{effectiveCancelText}
</button>
<button
onClick={onConfirm}
className={`flex-1 py-4 rounded-2xl bg-gray-50 text-[17px] font-medium active:bg-gray-100 transition-colors ${
isDestructive ? 'text-red-500' : 'text-blue-500'
}`}
>
{effectiveConfirmText}
</button>
</div>
</div>
</div>
);
};
// ============================================================================
// Action Menu (Popup)
// ============================================================================
interface ActionMenuProps {
open: boolean;
onClose: () => void;
options: {
label: string;
onClick: () => void;
destructive?: boolean;
}[];
}
export const ActionMenu: React.FC<ActionMenuProps> = ({ open, onClose, options }) => {
if (!open) return null;
return (
<div className="fixed inset-0 z-[100] flex items-end justify-end pb-24 pr-4 pointer-events-none">
<div
className="fixed inset-0 bg-black/5 pointer-events-auto"
onClick={onClose}
/>
<div className="relative bg-app-surface rounded-[24px] w-[200px] overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.12)] border border-gray-100 animate-in fade-in slide-in-from-bottom-4 duration-200 pointer-events-auto">
<div className="flex flex-col py-2">
{options.map((opt, index) => (
<button
key={index}
onClick={opt.onClick}
className={`w-full px-6 py-3.5 text-left text-[16px] active:bg-gray-50 transition-colors ${
opt.destructive ? 'text-red-500' : 'text-app-text'
}`}
>
{opt.label}
</button>
))}
</div>
</div>
</div>
);
};
// ============================================================================
// File Details Dialog
// ============================================================================
export const FileDetailsDialog: React.FC<{
open: boolean;
onClose: () => void;
file: FSNode | null;
}> = ({ open, onClose, file }) => {
const s = useAppStrings(strings, stringsEn);
if (!open || !file) return null;
const formatDate = (timestamp: number) => {
const date = TimeService.fromTimestamp(timestamp);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
};
const isFolder = file.type === 'directory';
return (
<div className="fixed inset-0 z-50 flex items-end justify-center pb-4 px-3">
<div className="absolute inset-0 bg-black/40 transition-opacity" onClick={onClose} />
<div className="relative bg-app-surface rounded-[32px] w-full overflow-hidden shadow-2xl animate-in fade-in slide-in-from-bottom-4 duration-200">
{/* Header with icon */}
<div className="pt-10 pb-4 flex flex-col items-center">
<div className="w-24 h-24 flex items-center justify-center mb-4">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 20H4C2.89543 20 2 19.1046 2 18V6C2 4.89543 2.89543 4 4 4H9.17157C9.70201 4 10.2107 4.21071 10.5858 4.58579L12.4142 6.41421C12.7893 6.78929 13.298 7 13.8284 7H20C21.1046 7 22 7.89543 22 9V18C22 19.1046 21.1046 20 20 20Z" fill="#FFC107"/>
</svg>
</div>
<h3 className="text-[22px] font-normal text-app-text text-center px-8 break-all">
{file.name}
</h3>
</div>
{/* Details */}
<div className="px-8 py-4 space-y-4 text-[18px]">
<div className="flex items-start gap-1">
<span className="text-app-text shrink-0">{s.detail_location}</span>
<span className="text-app-primary break-all leading-snug">
{file.path.replace('/sdcard', '/storage/emulated/0')}
</span>
</div>
<div className="flex items-center gap-1">
<span className="text-app-text shrink-0">{s.detail_size}</span>
<span className="text-app-text">{FileSystem.formatFileSize(file.size)}</span>
</div>
<div className="flex items-center gap-1">
<span className="text-app-text shrink-0">{s.detail_time}</span>
<span className="text-app-text">{formatDate(file.modifiedAt)}</span>
</div>
<div className="flex items-center gap-1">
<span className="text-app-text shrink-0">{s.detail_readable}</span>
<span className="text-app-text">{s.detail_yes}</span>
</div>
<div className="flex items-center gap-1">
<span className="text-app-text shrink-0">{s.detail_writable}</span>
<span className="text-app-text">{file.storage === 'preset' ? s.detail_no : s.detail_yes}</span>
</div>
<div className="flex items-center gap-1">
<span className="text-app-text shrink-0">{s.detail_hidden}</span>
<span className="text-app-text">{file.name.startsWith('.') ? s.detail_yes : s.detail_no}</span>
</div>
</div>
{/* Button */}
<div className="px-6 pb-8 mt-2">
<button
onClick={onClose}
className="w-full py-4 rounded-[24px] bg-[#f2f2f2] text-[20px] font-normal text-app-text active:opacity-60 transition-opacity"
>
{s.dialog_got_it}
</button>
</div>
</div>
</div>
);
};