Files
gitlab-instance-0a899031_d2…/mobile/app/(tabs)/index.tsx
eric b77a8cadb2
Some checks failed
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled
'init'
2026-03-24 14:46:41 -05:00

411 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from 'react'
import {
View,
Text,
ScrollView,
TextInput,
Pressable,
StyleSheet,
ActivityIndicator,
} from 'react-native'
import { Picker } from '@react-native-picker/picker'
import { SafeAreaView } from 'react-native-safe-area-context'
import { useBind } from '@/context/BindContext'
import { useRecorder } from '@/hooks/useRecorder'
import { theme } from '@/constants/theme'
function ctrlStyle(action: string): object {
switch (action) {
case 'start':
return styles.ctrl_start
case 'stop':
return styles.ctrl_stop
case 'restart':
return styles.ctrl_restart
case 'status':
return styles.ctrl_status
default:
return {}
}
}
const variantColor: Record<string, string> = {
online: theme.success,
stopped: theme.danger,
error: theme.danger,
warn: theme.warn,
offline: theme.danger,
loading: theme.muted,
unknown: theme.accent,
empty: theme.accent,
}
export default function RecordScreen() {
const { bind } = useBind()
const r = useRecorder()
const statusColor = useMemo(() => variantColor[r.statusMeta.variant] ?? theme.muted, [r.statusMeta.variant])
if (r.bootError && bind) {
return (
<SafeAreaView style={styles.safe}>
<Text style={styles.err}>无法连接:{r.bootError}</Text>
</SafeAreaView>
)
}
if (!bind) {
return (
<SafeAreaView style={styles.safe}>
<Text style={styles.hint}>请先在「配对」页扫码绑定 PC。</Text>
</SafeAreaView>
)
}
return (
<SafeAreaView style={styles.safe} edges={['bottom']}>
<ScrollView contentContainerStyle={styles.scroll}>
<View style={styles.toolbar}>
<Text style={styles.label}>当前录制</Text>
<View style={styles.pickerWrap}>
<Picker
enabled={!r.emptyHint}
selectedValue={r.currentProcess}
onValueChange={(v) => r.setCurrentProcess(String(v))}
style={styles.picker}
dropdownIconColor={theme.text}
>
{r.entries.map((e) => (
<Picker.Item key={e.pm2} label={e.label} value={e.pm2} color={theme.text} />
))}
</Picker>
</View>
{r.isYoutube && (
<Pressable
style={styles.proRow}
onPress={() => void r.togglePro(!r.multiChannelPro)}
>
<Text style={styles.proLabel}>Pro 转播</Text>
<Text style={[styles.proVal, r.multiChannelPro && { color: theme.accent }]}>
{r.multiChannelPro ? '开' : '关'}
</Text>
</Pressable>
)}
</View>
<View style={[styles.pill, { borderColor: statusColor }]}>
<View style={[styles.dot, { backgroundColor: statusColor }]} />
<Text style={[styles.pillText, { color: statusColor }]}>{r.statusMeta.line}</Text>
</View>
{r.emptyHint ? (
<Text style={styles.hint}>{r.emptyHint}</Text>
) : (
<>
{r.ent && (
<View style={styles.meta}>
<Text style={styles.metaLabel}>任务</Text>
<Text style={styles.metaName}>{r.ent.label}</Text>
<Text style={styles.metaFile}>{r.ent.script}</Text>
</View>
)}
{r.isYoutube && r.multiChannelPro && (
<View style={styles.card}>
<Text style={styles.cardTitle}>Pro 线路</Text>
{r.proChannelsError ? (
<Text style={styles.err}>{r.proChannelsError}</Text>
) : (
<View style={styles.pickerWrap}>
<Picker
selectedValue={r.proChannelId}
onValueChange={(v) => r.setProChannelId(String(v))}
style={styles.picker}
dropdownIconColor={theme.text}
>
{r.proChannelsList.map((c) => (
<Picker.Item
key={c.id}
label={`${c.name} (${c.keyPreview || '无密钥'})`}
value={c.id}
color={theme.text}
/>
))}
</Picker>
</View>
)}
</View>
)}
{r.isYoutube && r.multiChannelPro && !r.proChannelsError && r.proChannelsList.length > 0 && (
<>
<View style={styles.card}>
<Text style={styles.cardTitle}>YouTube 密钥(当前线路)</Text>
<TextInput
style={styles.area}
multiline
value={r.proYoutubeKey}
onChangeText={r.setProYoutubeKey}
placeholderTextColor={theme.muted}
/>
<Pressable
style={[styles.btn, r.saveProKeyLoading && styles.btnDisabled]}
disabled={r.saveProKeyLoading}
onPress={() => void r.saveProChannelKey()}
>
{r.saveProKeyLoading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.btnText}>保存密钥</Text>
)}
</Pressable>
<Text style={styles.hintSmall}>{r.youtubeStatus}</Text>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>地址列表(当前线路)</Text>
<TextInput
style={styles.area}
multiline
value={r.urlText}
onChangeText={r.setUrlText}
placeholderTextColor={theme.muted}
/>
<View style={styles.row}>
<Pressable
style={[styles.btn, styles.btnSec]}
onPress={() => void r.saveProUrlConfig()}
disabled={r.saveUrlLoading}
>
<Text style={styles.btnSecText}>{r.saveUrlLoading ? '…' : '保存地址'}</Text>
</Pressable>
</View>
<Text style={styles.hintSmall}>{r.urlStatus}</Text>
</View>
</>
)}
{r.isYoutube && !r.multiChannelPro && (
<View style={styles.card}>
<Text style={styles.cardTitle}>youtube.ini</Text>
<TextInput
style={styles.area}
multiline
value={r.youtubeText}
onChangeText={r.setYoutubeText}
placeholderTextColor={theme.muted}
/>
<View style={styles.row}>
<Pressable
style={[styles.btn, styles.btnSec]}
onPress={() => void r.loadConfig('/get_config', r.setYoutubeText, r.setYoutubeStatus)}
>
<Text style={styles.btnSecText}>重新读取</Text>
</Pressable>
<Pressable
style={[styles.btn, r.saveYoutubeLoading && styles.btnDisabled]}
disabled={r.saveYoutubeLoading}
onPress={() => {
r.setSaveYoutubeLoading(true)
void r.saveConfig('/save_config', r.youtubeText, r.setYoutubeStatus, () =>
r.setSaveYoutubeLoading(false),
)
}}
>
<Text style={styles.btnText}>保存</Text>
</Pressable>
</View>
<Text style={styles.hintSmall}>{r.youtubeStatus}</Text>
{!r.UI_SHOW_YOUTUBE_GOOGLE_OAUTH && (
<Text style={styles.hintSmall}>Google 授权与拉取密钥请在 PC 端操作。</Text>
)}
</View>
)}
{((r.isYoutube && !r.multiChannelPro) || r.isTiktok) && (
<View style={styles.card}>
<Text style={styles.cardTitle}>直播地址列表</Text>
<TextInput
style={styles.area}
multiline
value={r.urlText}
onChangeText={r.setUrlText}
placeholderTextColor={theme.muted}
/>
<View style={styles.row}>
<Pressable
style={[styles.btn, styles.btnSec]}
onPress={() => void r.loadConfig('/get_url_config', r.setUrlText, r.setUrlStatus)}
>
<Text style={styles.btnSecText}>重新读取</Text>
</Pressable>
<Pressable
style={[styles.btn, r.saveUrlLoading && styles.btnDisabled]}
disabled={r.saveUrlLoading}
onPress={() => {
r.setSaveUrlLoading(true)
void r.saveConfig('/save_url_config', r.urlText, r.setUrlStatus, () =>
r.setSaveUrlLoading(false),
)
}}
>
<Text style={styles.btnText}>保存</Text>
</Pressable>
</View>
<Text style={styles.hintSmall}>{r.urlStatus}</Text>
</View>
)}
{!r.UI_SHOW_YOUTUBE_GOOGLE_OAUTH && r.isObs && (
<View style={styles.card}>
<Text style={styles.cardTitle}>OBS 推流</Text>
<Text style={styles.mono}>srt://live.local:10080/live/obs</Text>
</View>
)}
<View style={[styles.card, styles.cardAccent]}>
<Text style={styles.cardTitle}>录制开关</Text>
<View style={styles.grid}>
{(
[
['start', '开始录制'],
['stop', '停止录制'],
['restart', '重新开始'],
['status', '刷新状态'],
] as const
).map(([action, label]) => (
<Pressable
key={action}
style={[styles.ctrl, ctrlStyle(action), r.loadingAction === action && styles.btnDisabled]}
disabled={r.loadingAction !== null && action !== 'status'}
onPress={() => void r.runPm2Action(action)}
>
<Text
style={[
styles.ctrlText,
action === 'status' && { color: theme.text },
]}
>
{label}
</Text>
</Pressable>
))}
</View>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>运行记录</Text>
<ScrollView style={styles.logBox} nestedScrollEnabled>
<Text style={styles.log}>{r.logText || '—'}</Text>
</ScrollView>
</View>
</>
)}
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: theme.bg },
scroll: { padding: 16, paddingBottom: 32 },
toolbar: { marginBottom: 12 },
label: { fontSize: 12, color: theme.muted, marginBottom: 6 },
pickerWrap: {
borderWidth: 1,
borderColor: theme.border,
borderRadius: 10,
overflow: 'hidden',
backgroundColor: theme.bgCard,
},
picker: { color: theme.text },
proRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 12,
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: theme.bgCard,
borderRadius: 10,
borderWidth: 1,
borderColor: theme.border,
},
proLabel: { color: theme.text, fontWeight: '600' },
proVal: { color: theme.muted, fontWeight: '600' },
pill: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
gap: 8,
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 999,
borderWidth: 1,
marginBottom: 14,
},
dot: { width: 8, height: 8, borderRadius: 4 },
pillText: { fontSize: 13, fontWeight: '600' },
meta: {
padding: 12,
borderRadius: 10,
backgroundColor: theme.bgElevated,
borderWidth: 1,
borderColor: theme.border,
marginBottom: 12,
},
metaLabel: { fontSize: 12, color: theme.muted },
metaName: { fontSize: 15, fontWeight: '600', color: theme.text, marginTop: 4 },
metaFile: { fontSize: 11, fontFamily: 'monospace', color: theme.muted, marginTop: 4 },
card: {
padding: 14,
borderRadius: 12,
backgroundColor: theme.bgCard,
borderWidth: 1,
borderColor: theme.border,
marginBottom: 12,
},
cardAccent: {
borderColor: 'rgba(91,157,255,0.35)',
backgroundColor: 'rgba(91,157,255,0.06)',
},
cardTitle: { fontSize: 15, fontWeight: '600', color: theme.text, marginBottom: 10 },
area: {
minHeight: 100,
borderWidth: 1,
borderColor: theme.border,
borderRadius: 10,
padding: 10,
color: theme.text,
fontFamily: 'monospace',
fontSize: 12,
marginBottom: 10,
backgroundColor: theme.bg,
},
row: { flexDirection: 'row', gap: 10, flexWrap: 'wrap' },
btn: {
backgroundColor: '#3d7eff',
paddingVertical: 10,
paddingHorizontal: 18,
borderRadius: 10,
minWidth: 100,
alignItems: 'center',
},
btnDisabled: { opacity: 0.55 },
btnText: { color: '#fff', fontWeight: '700' },
btnSec: { backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border },
btnSecText: { color: theme.text, fontWeight: '600' },
hint: { color: theme.muted, fontSize: 14, lineHeight: 22 },
hintSmall: { color: theme.muted, fontSize: 12, marginTop: 8 },
err: { color: theme.danger, fontSize: 14 },
mono: { fontFamily: 'monospace', color: theme.accent, fontSize: 13 },
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
ctrl: { flexGrow: 1, minWidth: '42%', paddingVertical: 14, borderRadius: 10, alignItems: 'center' },
ctrl_start: { backgroundColor: '#0e7a0d' },
ctrl_stop: { backgroundColor: '#d1342c' },
ctrl_restart: { backgroundColor: '#e68600' },
ctrl_status: { backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border },
ctrlText: { color: '#fff', fontWeight: '700', fontSize: 14 },
logBox: { maxHeight: 280, backgroundColor: '#1e2128', borderRadius: 10, padding: 10 },
log: { fontFamily: 'monospace', fontSize: 11, color: '#d0d4dc', lineHeight: 18 },
})