Add embedded about & open-source credits screen (license audit)
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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.0.0'; // 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>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, Modal, TouchableOpacity } from 'react-native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { X, Volume2, Settings as SettingsIcon } from 'lucide-react-native';
|
||||
import { X, Volume2, Info, ChevronRight, Settings as SettingsIcon } from 'lucide-react-native';
|
||||
import { AspectRatio, MeterMode, ShutterSound, StartupMode } from '../types';
|
||||
import CreditsModal from './CreditsModal';
|
||||
|
||||
// Gear-sheet (gear button between library picker and shutter). Groups the
|
||||
// capture/composition choices that are NOT look-adjustments, so the tool rail
|
||||
@@ -90,6 +91,7 @@ export default function SettingsModal({
|
||||
startupMode,
|
||||
onStartupMode,
|
||||
}: SettingsModalProps) {
|
||||
const [showCredits, setShowCredits] = useState(false);
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||
<View className="flex-1 bg-black/70 items-center justify-center px-6">
|
||||
@@ -189,8 +191,32 @@ export default function SettingsModal({
|
||||
/>
|
||||
<CameraOnlyNote cameraMode={cameraMode} />
|
||||
</View>
|
||||
|
||||
{/* About & open-source credits */}
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
haptic();
|
||||
setShowCredits(true);
|
||||
}}
|
||||
className="flex-row items-center justify-between rounded-xl border border-zinc-800/70 bg-black/30 px-3 py-2.5"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Info size={13} color="#a1a1aa" strokeWidth={1.75} />
|
||||
<Text className="text-zinc-400 font-mono text-[10px] font-bold tracking-[0.2em] ml-2">
|
||||
ABOUT & CREDITS
|
||||
</Text>
|
||||
</View>
|
||||
<ChevronRight size={14} color="#52525b" />
|
||||
</TouchableOpacity>
|
||||
<Text className="text-zinc-600 font-mono text-[10px] mt-1.5 ml-1">
|
||||
App info + third-party open-source licenses.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<CreditsModal visible={showCredits} onClose={() => setShowCredits(false)} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user