mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-16 15:56:31 +00:00
feat(mal): added remove button, rewatching support, and rich profile dashboard
This commit is contained in:
parent
9122ff5345
commit
c9fd5438b4
7 changed files with 392 additions and 86 deletions
|
|
@ -10,6 +10,8 @@ import {
|
|||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -36,13 +38,16 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
const [status, setStatus] = useState<MalListStatus>(anime.list_status.status);
|
||||
const [episodes, setEpisodes] = useState(anime.list_status.num_episodes_watched.toString());
|
||||
const [score, setScore] = useState(anime.list_status.score.toString());
|
||||
const [isRewatching, setIsRewatching] = useState(anime.list_status.is_rewatching || false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setStatus(anime.list_status.status);
|
||||
setEpisodes(anime.list_status.num_episodes_watched.toString());
|
||||
setScore(anime.list_status.score.toString());
|
||||
setIsRewatching(anime.list_status.is_rewatching || false);
|
||||
}
|
||||
}, [visible, anime]);
|
||||
|
||||
|
|
@ -55,7 +60,7 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
// Validation: MAL scores must be between 0 and 10
|
||||
scoreNum = Math.max(0, Math.min(10, scoreNum));
|
||||
|
||||
await MalApiService.updateStatus(anime.node.id, status, epNum, scoreNum);
|
||||
await MalApiService.updateStatus(anime.node.id, status, epNum, scoreNum, isRewatching);
|
||||
|
||||
showSuccess('Updated', `${anime.node.title} status updated on MAL`);
|
||||
onUpdateSuccess();
|
||||
|
|
@ -67,6 +72,33 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
Alert.alert(
|
||||
'Remove from List',
|
||||
`Are you sure you want to remove ${anime.node.title} from your MyAnimeList?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Remove',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setIsRemoving(true);
|
||||
try {
|
||||
await MalApiService.removeFromList(anime.node.id);
|
||||
showSuccess('Removed', `${anime.node.title} removed from MAL`);
|
||||
onUpdateSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
showError('Remove Failed', 'Could not remove from MAL');
|
||||
} finally {
|
||||
setIsRemoving(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const statusOptions: { label: string; value: MalListStatus }[] = [
|
||||
{ label: 'Watching', value: 'watching' },
|
||||
{ label: 'Completed', value: 'completed' },
|
||||
|
|
@ -160,10 +192,25 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.rewatchRow}>
|
||||
<View style={styles.rewatchTextContainer}>
|
||||
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis, marginTop: 0 }]}>Rewatching</Text>
|
||||
<Text style={[styles.rewatchDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Mark this if you are watching the series again.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={isRewatching}
|
||||
onValueChange={setIsRewatching}
|
||||
trackColor={{ false: currentTheme.colors.border, true: currentTheme.colors.primary + '80' }}
|
||||
thumbColor={isRewatching ? currentTheme.colors.primary : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.updateButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={handleUpdate}
|
||||
disabled={isUpdating}
|
||||
disabled={isUpdating || isRemoving}
|
||||
>
|
||||
{isUpdating ? (
|
||||
<ActivityIndicator color="white" />
|
||||
|
|
@ -171,6 +218,20 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
<Text style={styles.updateButtonText}>Update MAL</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.removeButton, { borderColor: currentTheme.colors.error || '#FF5252' }]}
|
||||
onPress={handleRemove}
|
||||
disabled={isUpdating || isRemoving}
|
||||
>
|
||||
{isRemoving ? (
|
||||
<ActivityIndicator color={currentTheme.colors.error || '#FF5252'} />
|
||||
) : (
|
||||
<Text style={[styles.removeButtonText, { color: currentTheme.colors.error || '#FF5252' }]}>
|
||||
Remove from List
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
|
@ -245,6 +306,21 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
rewatchRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 20,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
rewatchTextContainer: {
|
||||
flex: 1,
|
||||
marginRight: 16,
|
||||
},
|
||||
rewatchDescription: {
|
||||
fontSize: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
updateButton: {
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
|
|
@ -258,4 +334,17 @@ const styles = StyleSheet.create({
|
|||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
removeButton: {
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
marginBottom: 20,
|
||||
borderWidth: 1,
|
||||
},
|
||||
removeButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1487,7 +1487,7 @@ const LibraryScreen = () => {
|
|||
const currentOffset = isLoadMore ? malOffset : 0;
|
||||
setMalLoading(true);
|
||||
try {
|
||||
const response = await MalApiService.getUserList(undefined, currentOffset);
|
||||
const response = await MalApiService.getUserList(undefined, currentOffset, 100);
|
||||
if (isLoadMore) {
|
||||
setMalMalList(prev => [...prev, ...response.data]);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -187,61 +187,68 @@ const MalLibraryScreen: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
|
||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
{/* Requirement 6: Manual Sync Button */}
|
||||
<TouchableOpacity onPress={handleRefresh} style={styles.syncButton} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
) : (
|
||||
<MaterialIcons name="sync" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{!isLoading || isRefreshing ? (
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} tintColor={currentTheme.colors.primary} />
|
||||
}
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
>
|
||||
{renderSection('watching', 'Watching', 'play-circle-outline')}
|
||||
{renderSection('plan_to_watch', 'Plan to Watch', 'bookmark-outline')}
|
||||
{renderSection('completed', 'Completed', 'check-circle-outline')}
|
||||
{renderSection('on_hold', 'On Hold', 'pause-circle-outline')}
|
||||
{renderSection('dropped', 'Dropped', 'highlight-off')}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
backgroundColor="transparent"
|
||||
translucent
|
||||
/>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
|
||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
{/* Requirement 6: Manual Sync Button */}
|
||||
<TouchableOpacity onPress={handleRefresh} style={styles.syncButton} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
) : (
|
||||
<MaterialIcons name="sync" size={24} color={currentTheme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{selectedAnime && (
|
||||
<MalEditModal
|
||||
visible={isEditModalVisible}
|
||||
anime={selectedAnime}
|
||||
onClose={() => {
|
||||
setIsEditModalVisible(false);
|
||||
setSelectedAnime(null);
|
||||
}}
|
||||
onUpdateSuccess={fetchMalList}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
{!isLoading || isRefreshing ? (
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} tintColor={currentTheme.colors.primary} />
|
||||
}
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
>
|
||||
{renderSection('watching', 'Watching', 'play-circle-outline')}
|
||||
{renderSection('plan_to_watch', 'Plan to Watch', 'bookmark-outline')}
|
||||
{renderSection('completed', 'Completed', 'check-circle-outline')}
|
||||
{renderSection('on_hold', 'On Hold', 'pause-circle-outline')}
|
||||
{renderSection('dropped', 'Dropped', 'highlight-off')}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{selectedAnime && (
|
||||
<MalEditModal
|
||||
visible={isEditModalVisible}
|
||||
anime={selectedAnime}
|
||||
onClose={() => {
|
||||
setIsEditModalVisible(false);
|
||||
setSelectedAnime(null);
|
||||
}}
|
||||
onUpdateSuccess={fetchMalList}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
safeArea: { flex: 1, paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const MalSettingsScreen: React.FC = () => {
|
|||
const [syncEnabled, setSyncEnabled] = useState(mmkvStorage.getBoolean('mal_enabled') ?? true);
|
||||
const [autoUpdateEnabled, setAutoUpdateEnabled] = useState(mmkvStorage.getBoolean('mal_auto_update') ?? true);
|
||||
const [autoAddEnabled, setAutoAddEnabled] = useState(mmkvStorage.getBoolean('mal_auto_add') ?? true);
|
||||
const [autoLibrarySyncEnabled, setAutoLibrarySyncEnabled] = useState(mmkvStorage.getBoolean('mal_auto_sync_to_library') ?? false);
|
||||
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
|
|
@ -139,6 +140,11 @@ const MalSettingsScreen: React.FC = () => {
|
|||
mmkvStorage.setBoolean('mal_auto_add', val);
|
||||
};
|
||||
|
||||
const toggleAutoLibrarySync = (val: boolean) => {
|
||||
setAutoLibrarySyncEnabled(val);
|
||||
mmkvStorage.setBoolean('mal_auto_sync_to_library', val);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[
|
||||
styles.container,
|
||||
|
|
@ -188,42 +194,117 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<Text style={[styles.profileName, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.name}
|
||||
</Text>
|
||||
<Text style={[styles.profileUsername, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
ID: {userProfile.id}
|
||||
</Text>
|
||||
<View style={styles.profileDetailRow}>
|
||||
<MaterialIcons name="fingerprint" size={14} color={currentTheme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.profileDetailText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
ID: {userProfile.id}
|
||||
</Text>
|
||||
</View>
|
||||
{userProfile.location && (
|
||||
<View style={styles.profileDetailRow}>
|
||||
<MaterialIcons name="location-on" size={14} color={currentTheme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.profileDetailText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{userProfile.location}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{userProfile.birthday && (
|
||||
<View style={styles.profileDetailRow}>
|
||||
<MaterialIcons name="cake" size={14} color={currentTheme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.profileDetailText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{userProfile.birthday}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.signOutButton, { backgroundColor: currentTheme.colors.error }]}
|
||||
onPress={handleSignOut}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary, marginTop: 12 }]}
|
||||
onPress={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const synced = await MalSync.syncMalToLibrary();
|
||||
if (synced) {
|
||||
openAlert('Sync Complete', 'MAL data has been refreshed.');
|
||||
} else {
|
||||
openAlert('Sync Failed', 'Could not refresh MAL data.');
|
||||
}
|
||||
} catch {
|
||||
openAlert('Sync Failed', 'Could not refresh MAL data.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<MaterialIcons name="sync" size={20} color="white" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.buttonText}>Sync Now</Text>
|
||||
{userProfile.anime_statistics && (
|
||||
<View style={styles.statsContainer}>
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
|
||||
{userProfile.anime_statistics.num_items}
|
||||
</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Total</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
|
||||
{userProfile.anime_statistics.num_days_watched.toFixed(1)}
|
||||
</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Days</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
|
||||
{userProfile.anime_statistics.mean_score.toFixed(1)}
|
||||
</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Mean</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statGrid, { borderColor: currentTheme.colors.border }]}>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#2DB039' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Watching</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_watching}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#26448F' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Completed</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_completed}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#F9D457' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>On Hold</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_on_hold}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#A12F31' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Dropped</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_dropped}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={styles.actionButtonsRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.smallButton, { backgroundColor: currentTheme.colors.primary, flex: 1, marginRight: 8 }]}
|
||||
onPress={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const synced = await MalSync.syncMalToLibrary();
|
||||
if (synced) {
|
||||
openAlert('Sync Complete', 'MAL data has been refreshed.');
|
||||
} else {
|
||||
openAlert('Sync Failed', 'Could not refresh MAL data.');
|
||||
}
|
||||
} catch {
|
||||
openAlert('Sync Failed', 'Could not refresh MAL data.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="sync" size={18} color="white" style={{ marginRight: 6 }} />
|
||||
<Text style={styles.buttonText}>Sync</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.smallButton, { backgroundColor: currentTheme.colors.error, width: 100 }]}
|
||||
onPress={handleSignOut}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.signInContainer}>
|
||||
|
|
@ -311,6 +392,25 @@ const MalSettingsScreen: React.FC = () => {
|
|||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Auto-Sync to Library
|
||||
</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Automatically add items from your MAL 'Watching' list to your Nuvio Library.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={autoLibrarySyncEnabled}
|
||||
onValueChange={toggleAutoLibrarySync}
|
||||
trackColor={{ false: currentTheme.colors.border, true: currentTheme.colors.primary + '80' }}
|
||||
thumbColor={autoLibrarySyncEnabled ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -371,7 +471,37 @@ const styles = StyleSheet.create({
|
|||
avatarText: { fontSize: 24, color: 'white', fontWeight: 'bold' },
|
||||
profileInfo: { marginLeft: 16, flex: 1 },
|
||||
profileName: { fontSize: 18, fontWeight: '600' },
|
||||
profileUsername: { fontSize: 14 },
|
||||
profileDetailRow: { flexDirection: 'row', alignItems: 'center', marginTop: 2 },
|
||||
profileDetailText: { fontSize: 12, marginLeft: 4 },
|
||||
statsContainer: { marginTop: 20 },
|
||||
statsRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16 },
|
||||
statBox: { alignItems: 'center', flex: 1 },
|
||||
statValue: { fontSize: 18, fontWeight: 'bold' },
|
||||
statLabel: { fontSize: 12, marginTop: 2 },
|
||||
statGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
borderTopWidth: 1,
|
||||
paddingTop: 16,
|
||||
gap: 12
|
||||
},
|
||||
statGridItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: '45%',
|
||||
marginBottom: 8
|
||||
},
|
||||
statusDot: { width: 8, height: 8, borderRadius: 4, marginRight: 8 },
|
||||
statGridLabel: { fontSize: 13, flex: 1 },
|
||||
statGridValue: { fontSize: 13, fontWeight: '600' },
|
||||
actionButtonsRow: { flexDirection: 'row', marginTop: 20 },
|
||||
smallButton: {
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
signOutButton: { marginTop: 20 },
|
||||
settingsSection: { padding: 20 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '600', marginBottom: 16 },
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const MalApiService = {
|
|||
limit,
|
||||
offset,
|
||||
sort: 'list_updated_at',
|
||||
nsfw: true // Ensure all content is returned
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
|
|
@ -64,11 +65,13 @@ export const MalApiService = {
|
|||
malId: number,
|
||||
status: MalListStatus,
|
||||
episode: number,
|
||||
score?: number
|
||||
score?: number,
|
||||
isRewatching?: boolean
|
||||
) => {
|
||||
const data: any = {
|
||||
status,
|
||||
num_watched_episodes: episode,
|
||||
is_rewatching: isRewatching || false
|
||||
};
|
||||
if (score && score > 0) data.score = score;
|
||||
|
||||
|
|
@ -77,6 +80,10 @@ export const MalApiService = {
|
|||
});
|
||||
},
|
||||
|
||||
removeFromList: async (malId: number) => {
|
||||
return api.delete(`/anime/${malId}/my_list_status`);
|
||||
},
|
||||
|
||||
getAnimeDetails: async (malId: number) => {
|
||||
try {
|
||||
const response = await api.get(`/anime/${malId}`, {
|
||||
|
|
@ -91,7 +98,11 @@ export const MalApiService = {
|
|||
|
||||
getUserInfo: async (): Promise<MalUser> => {
|
||||
try {
|
||||
const response = await api.get('/users/@me');
|
||||
const response = await api.get('/users/@me', {
|
||||
params: {
|
||||
fields: 'id,name,picture,gender,birthday,location,joined_at,anime_statistics,time_zone'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get user info', error);
|
||||
|
|
|
|||
|
|
@ -349,6 +349,12 @@ export const MalSync = {
|
|||
mmkvStorage.setNumber(getLegacyTitleCacheKey(title, type), item.node.id);
|
||||
}
|
||||
console.log(`[MalSync] Synced ${allItems.length} items to mapping cache.`);
|
||||
|
||||
// If auto-sync to library is enabled, also add 'watching' items to Nuvio Library
|
||||
if (mmkvStorage.getBoolean('mal_auto_sync_to_library') ?? false) {
|
||||
await MalSync.syncMalWatchingToLibrary();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('syncMalToLibrary failed', e);
|
||||
|
|
@ -356,6 +362,49 @@ export const MalSync = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Automatically adds MAL 'watching' items to the Nuvio Library
|
||||
*/
|
||||
syncMalWatchingToLibrary: async () => {
|
||||
try {
|
||||
logger.log('[MalSync] Auto-syncing MAL watching items to library...');
|
||||
|
||||
const response = await MalApiService.getUserList('watching', 0, 50);
|
||||
if (!response.data || response.data.length === 0) return;
|
||||
|
||||
for (const item of response.data) {
|
||||
const malId = item.node.id;
|
||||
const { imdbId } = await MalSync.getIdsFromMalId(malId);
|
||||
|
||||
if (imdbId) {
|
||||
const type = item.node.media_type === 'movie' ? 'movie' : 'series';
|
||||
|
||||
// Check if already in library to avoid redundant calls
|
||||
const currentLibrary = await catalogService.getLibraryItems();
|
||||
const exists = currentLibrary.some(l => l.id === imdbId);
|
||||
|
||||
if (!exists) {
|
||||
logger.log(`[MalSync] Auto-adding to library: ${item.node.title} (${imdbId})`);
|
||||
|
||||
await catalogService.addToLibrary({
|
||||
id: imdbId,
|
||||
type: type,
|
||||
name: item.node.title,
|
||||
poster: item.node.main_picture?.large || item.node.main_picture?.medium || '',
|
||||
posterShape: 'poster',
|
||||
year: item.node.start_season?.year,
|
||||
description: '',
|
||||
genres: [],
|
||||
inLibrary: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[MalSync] syncMalWatchingToLibrary failed:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Manually map an ID if auto-detection fails
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,8 +9,28 @@ export interface MalUser {
|
|||
id: number;
|
||||
name: string;
|
||||
picture?: string;
|
||||
gender?: string;
|
||||
birthday?: string;
|
||||
location?: string;
|
||||
joined_at?: string;
|
||||
time_zone?: string;
|
||||
anime_statistics?: {
|
||||
num_items_watching: number;
|
||||
num_items_completed: number;
|
||||
num_items_on_hold: number;
|
||||
num_items_dropped: number;
|
||||
num_items_plan_to_watch: number;
|
||||
num_items: number;
|
||||
num_days_watched: number;
|
||||
num_days_watching: number;
|
||||
num_days_completed: number;
|
||||
num_days_on_hold: number;
|
||||
num_days_dropped: number;
|
||||
num_days: number;
|
||||
num_episodes: number;
|
||||
num_times_rewatched: number;
|
||||
mean_score: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MalAnime {
|
||||
|
|
|
|||
Loading…
Reference in a new issue