This commit is contained in:
Neil.X.Zhang
2025-06-27 16:16:14 +08:00
commit 3b79d06b7d
111 changed files with 20915 additions and 0 deletions

42
app/+html.tsx Normal file
View File

@@ -0,0 +1,42 @@
import {ScrollViewStyleReset} from 'expo-router/html';
import {type PropsWithChildren} from 'react';
/**
* This file is web-only and used to configure the root HTML for every web page during static rendering.
* The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs.
*/
export default function Root({children}: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{__html: responsiveBackground}} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

32
app/+not-found.tsx Normal file
View File

@@ -0,0 +1,32 @@
import {Link, Stack} from 'expo-router';
import {StyleSheet} from 'react-native';
import {ThemedText} from '@/components/ThemedText';
import {ThemedView} from '@/components/ThemedView';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{title: 'Oops!'}} />
<ThemedView style={styles.container}>
<ThemedText type="title">This screen doesn't exist.</ThemedText>
<Link href="/" style={styles.link}>
<ThemedText type="link">Go to home screen!</ThemedText>
</Link>
</ThemedView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
});

43
app/_layout.tsx Normal file
View File

@@ -0,0 +1,43 @@
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from "@react-navigation/native";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { useColorScheme } from "@/hooks/useColorScheme";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded, error] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
useEffect(() => {
if (loaded || error) {
SplashScreen.hideAsync();
if (error) {
console.warn(`Error in loading fonts: ${error}`);
}
}
}, [loaded, error]);
if (!loaded && !error) {
return null;
}
return (
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</ThemeProvider>
);
}

33
app/detail.tsx Normal file
View File

@@ -0,0 +1,33 @@
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import { useLocalSearchParams } from "expo-router";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
export default function DetailScreen() {
const { source, id } = useLocalSearchParams();
return (
<ThemedView style={styles.container}>
<ThemedText type="title">Detail Page</ThemedText>
<View style={styles.separator} />
<ThemedText>Source: {source}</ThemedText>
<ThemedText>ID: {id}</ThemedText>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
backgroundColor: "#666",
},
});

113
app/index.tsx Normal file
View File

@@ -0,0 +1,113 @@
import React, { useState, useEffect } from "react";
import { View, StyleSheet, ActivityIndicator, FlatList } from "react-native";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import ScrollableRow from "@/components/ScrollableRow.tv";
import { MoonTVAPI, DoubanResponse } from "@/services/api";
import { RowItem } from "@/components/ScrollableRow.tv";
interface ContentRow {
title: string;
data: RowItem[];
}
const categories = [
{ title: "热门电影", type: "movie", tag: "热门" },
{ title: "热门剧集", type: "tv", tag: "热门" },
{ title: "豆瓣 Top250", type: "movie", tag: "top250" },
{ title: "综艺", type: "tv", tag: "综艺" },
{ title: "美剧", type: "tv", tag: "美剧" },
{ title: "韩剧", type: "tv", tag: "韩剧" },
{ title: "日剧", type: "tv", tag: "日剧" },
{ title: "日漫", type: "tv", tag: "日本动画" },
] as const;
// --- IMPORTANT ---
// Replace with your computer's LAN IP address to test on a real device or emulator.
// Find it by running `ifconfig` (macOS/Linux) or `ipconfig` (Windows).
const API_BASE_URL = "http://192.168.31.123:3001";
const api = new MoonTVAPI(API_BASE_URL);
export default function HomeScreen() {
const [rows, setRows] = useState<ContentRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchAllData = async () => {
setLoading(true);
setError(null);
try {
const promises = categories.map((category) =>
api.getDoubanData(category.type, category.tag, 20)
);
const results = await Promise.all<DoubanResponse>(promises);
const newRows: ContentRow[] = results.map((result, index) => {
const category = categories[index];
return {
title: category.title,
data: result.list.map((item) => ({
...item,
id: item.title, // Use title as a temporary unique id
source: "douban", // Static source for douban items
})),
};
});
setRows(newRows);
} catch (err) {
console.error("Failed to fetch data for home screen:", err);
setError("无法加载内容,请稍后重试。");
} finally {
setLoading(false);
}
};
fetchAllData();
}, []);
if (loading) {
return (
<ThemedView style={styles.centerContainer}>
<ActivityIndicator size="large" />
</ThemedView>
);
}
if (error) {
return (
<ThemedView style={styles.centerContainer}>
<ThemedText type="subtitle">{error}</ThemedText>
</ThemedView>
);
}
return (
<ThemedView style={styles.container}>
<FlatList
data={rows}
renderItem={({ item }) => (
<ScrollableRow title={item.title} data={item.data} api={api} />
)}
keyExtractor={(item) => item.title}
contentContainerStyle={styles.listContent}
/>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
centerContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
listContent: {
paddingTop: 40,
paddingBottom: 40,
},
});