67a2a52a6f
versionName 1.2 / versionCode 2, kept in sync with app.json, package.json and the credits screen.
164 lines
6.3 KiB
TypeScript
164 lines
6.3 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
|
import { View, Text, Modal, ScrollView, TouchableOpacity, Linking } from 'react-native';
|
|
import { X, ChevronDown, ChevronRight, ExternalLink, Scale } from 'lucide-react-native';
|
|
import { CREDITS, LICENSE_TEXTS, CreditEntry } from '../utils/credits';
|
|
|
|
// About + open-source credits, reachable from the settings sheet. Third-party
|
|
// data is generated from node_modules (src/utils/credits.ts + THIRD_PARTY_NOTICES.md);
|
|
// each project row links to its homepage, each license group expands to the
|
|
// full license text embedded in the app.
|
|
|
|
const APP_NAME = 'recipescamera';
|
|
const APP_VERSION = '1.2'; // keep in sync with app.json
|
|
|
|
function Group({
|
|
license,
|
|
entries,
|
|
}: {
|
|
license: string;
|
|
entries: CreditEntry[];
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
const text = LICENSE_TEXTS[license];
|
|
return (
|
|
<View className="rounded-xl border border-zinc-800/70 bg-black/30 overflow-hidden">
|
|
<TouchableOpacity
|
|
onPress={() => setOpen((v) => !v)}
|
|
className="flex-row items-center justify-between px-3 py-2.5"
|
|
activeOpacity={0.7}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Scale size={12} color="#f59e0b" strokeWidth={1.75} />
|
|
<Text className="text-zinc-300 font-mono text-xs font-bold tracking-widest ml-2">
|
|
{license}
|
|
</Text>
|
|
<Text className="text-zinc-600 font-mono text-[10px] ml-2">
|
|
{entries.length} {entries.length === 1 ? 'component' : 'components'}
|
|
</Text>
|
|
</View>
|
|
{open ? (
|
|
<ChevronDown size={14} color="#71717a" />
|
|
) : (
|
|
<ChevronRight size={14} color="#71717a" />
|
|
)}
|
|
</TouchableOpacity>
|
|
{open && (
|
|
<View className="px-3 pb-3 space-y-2">
|
|
{entries.map((e) => (
|
|
<View key={e.name} className="border-t border-zinc-800/60 pt-2">
|
|
<View className="flex-row items-start justify-between">
|
|
<Text className="text-zinc-200 font-mono text-[11px] font-semibold flex-1 pr-2">
|
|
{e.name}
|
|
</Text>
|
|
{e.url.startsWith('http') && (
|
|
<TouchableOpacity
|
|
onPress={() => Linking.openURL(e.url)}
|
|
accessibilityLabel={`Open ${e.name} project page`}
|
|
className="flex-row items-center"
|
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
>
|
|
<ExternalLink size={12} color="#f59e0b" />
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
{e.copyright ? (
|
|
<Text className="text-zinc-500 font-mono text-[10px] mt-0.5">{e.copyright}</Text>
|
|
) : null}
|
|
<Text className="text-zinc-600 font-mono text-[10px] mt-0.5">
|
|
{e.version}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
{text ? (
|
|
<View className="border-t border-zinc-800/60 pt-2">
|
|
<Text selectable className="text-zinc-500 font-mono text-[9px] leading-4">
|
|
{text}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export default function CreditsModal({
|
|
visible,
|
|
onClose,
|
|
}: {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
}) {
|
|
const groups = useMemo(() => {
|
|
const byLicense = new Map<string, CreditEntry[]>();
|
|
for (const e of CREDITS) {
|
|
const arr = byLicense.get(e.license) ?? [];
|
|
arr.push(e);
|
|
byLicense.set(e.license, arr);
|
|
}
|
|
// stable order: licenses as declared in LICENSE_TEXTS, then rest
|
|
const order = Object.keys(LICENSE_TEXTS);
|
|
const sorted = [...byLicense.keys()].sort(
|
|
(a, b) => order.indexOf(a) - order.indexOf(b)
|
|
);
|
|
return sorted
|
|
.filter((k) => byLicense.has(k))
|
|
.map((k) => ({ license: k, entries: byLicense.get(k)! }));
|
|
}, []);
|
|
|
|
return (
|
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
|
<View className="flex-1 bg-black/70 items-center justify-center px-4">
|
|
<TouchableOpacity className="absolute inset-0" activeOpacity={1} onPress={onClose} />
|
|
<View
|
|
className="w-full max-w-md rounded-2xl border border-zinc-800/70 p-5"
|
|
style={{ backgroundColor: 'rgba(8,8,10,0.97)', maxHeight: '85%' }}
|
|
>
|
|
<View className="flex-row items-center justify-between mb-3">
|
|
<Text className="text-zinc-300 font-mono text-xs font-bold tracking-widest">
|
|
ABOUT & CREDITS
|
|
</Text>
|
|
<TouchableOpacity
|
|
onPress={onClose}
|
|
className="w-8 h-8 rounded-full bg-zinc-900 flex items-center justify-center"
|
|
activeOpacity={0.7}
|
|
>
|
|
<X size={16} color="#ffffff" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View className="rounded-xl border border-zinc-800/70 bg-black/30 px-3 py-2.5 mb-3">
|
|
<Text className="text-zinc-200 font-mono text-sm font-bold">{APP_NAME}</Text>
|
|
<Text className="text-zinc-500 font-mono text-[10px] mt-0.5">
|
|
version {APP_VERSION} · film-recipe camera (experimental)
|
|
</Text>
|
|
<View className="flex-row flex-wrap items-center mt-1">
|
|
<Text className="text-zinc-400 font-mono text-[10px]">Author: Lộc Phạm</Text>
|
|
<TouchableOpacity
|
|
onPress={() => Linking.openURL('mailto:tranloclqd@gmail.com')}
|
|
accessibilityLabel="Email author"
|
|
className="flex-row items-center ml-3"
|
|
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
|
>
|
|
<Text className="text-amber-500/90 font-mono text-[10px] underline">
|
|
tranloclqd@gmail.com
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<Text className="text-zinc-600 font-mono text-[10px] mt-1">
|
|
© 2026 Lộc Phạm · Built with React Native + Expo; icons by Lucide;
|
|
on-screen graphics rendered with Skia.
|
|
</Text>
|
|
</View>
|
|
|
|
<ScrollView className="space-y-2" showsVerticalScrollIndicator={false}>
|
|
{groups.map((g) => (
|
|
<Group key={g.license} license={g.license} entries={g.entries} />
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|