diff --git a/.vscode/settings.json b/.vscode/settings.json index 997989e7..418d8a40 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,6 +13,7 @@ "Prefs", "reduxjs", "temurin", - "tlyric" + "tlyric", + "userprofiles" ] } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 50859e50..c07fb907 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import * as SplashScreen from 'expo-splash-screen'; import React, { useEffect } from 'react'; import { AppContainer, RootStack } from './components'; import { useAppDispatch } from './hook'; -import { setBlurRadius, setFavs } from './redux/slices'; +import { setBlurRadius, setFavs, setUser } from './redux/slices'; SplashScreen.preventAutoHideAsync(); @@ -14,6 +14,8 @@ export enum StorageKeys { BlurRadius = 'blurRadius', // eslint-disable-next-line no-unused-vars Favs = 'favs', + // eslint-disable-next-line no-unused-vars + User = 'user', } export default function App() { @@ -30,6 +32,11 @@ export default function App() { if (storedFavs) { dispatch(setFavs(JSON.parse(storedFavs))); } + + const storedUser = await AsyncStorage.getItem(StorageKeys.User); + if (storedUser) { + dispatch(setUser(JSON.parse(storedUser))); + } } restorePrefs(); diff --git a/src/components/RootStack.tsx b/src/components/RootStack.tsx index f1a7cdec..71e7a42d 100644 --- a/src/components/RootStack.tsx +++ b/src/components/RootStack.tsx @@ -10,6 +10,7 @@ import { AlbumDetail, Artist, Comments, + Login, LyricsScreen, MvDetail, MvPlayer, @@ -51,6 +52,7 @@ export function RootStack() { + ); } diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx index 7de9f2c8..5dc7699e 100644 --- a/src/components/UserHeader.tsx +++ b/src/components/UserHeader.tsx @@ -1,12 +1,14 @@ +import { useNavigation } from '@react-navigation/native'; import React, { memo } from 'react'; -import { Dimensions, ImageBackground, StyleSheet, View } from 'react-native'; -import { Avatar, Text, useTheme } from 'react-native-paper'; +import { Dimensions, ImageBackground, StyleSheet } from 'react-native'; +import { Avatar, Text, TouchableRipple, useTheme } from 'react-native-paper'; import useSWR from 'swr'; import { useAppSelector } from '../hook'; import { selectUser } from '../redux/slices'; import { Main } from '../types/userDetail'; export const UserHeader = memo(({ userId }: { userId?: number }) => { + const navigation = useNavigation(); const appTheme = useTheme(); const currentUser = useAppSelector(selectUser); @@ -29,20 +31,26 @@ export const UserHeader = memo(({ userId }: { userId?: number }) => { imageStyle={styles.img} source={{ uri: data?.profile.backgroundUrl }} > - + borderless + onPress={() => { + //@ts-expect-error + navigation.push('Login'); + }} + > + + - - - {data?.profile.nickname} - - - {data?.profile.signature} - - + + {data?.profile.nickname} + + + {data?.profile.signature} + ); }); @@ -56,8 +64,6 @@ const styles = StyleSheet.create({ }, avatar: { marginTop: '30%', - }, - caption: { - marginTop: '1%' + borderRadius: 50, } }); diff --git a/src/components/UserItem.tsx b/src/components/UserItem.tsx new file mode 100644 index 00000000..e9722e13 --- /dev/null +++ b/src/components/UserItem.tsx @@ -0,0 +1,45 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import React from 'react'; +import { ToastAndroid } from 'react-native'; +import HapticFeedback, { HapticFeedbackTypes } from 'react-native-haptic-feedback'; +import { Avatar, List } from 'react-native-paper'; +import { StorageKeys } from '../App'; +import { useAppDispatch } from '../hook'; +import { setUser } from '../redux/slices'; +import { Userprofile } from '../types/searchUsers'; + +export const UserItem = ({ item }: { item: Userprofile; }) => { + const dispatch = useAppDispatch(); + + const login = () => { + const user = { + username: item.nickname, + id: item.userId + }; + + dispatch(setUser(user)); + AsyncStorage.setItem( + StorageKeys.User, JSON.stringify(user) + ); + + HapticFeedback.trigger( + HapticFeedbackTypes.effectHeavyClick + ); + ToastAndroid.show( + `Logged in as ${item.nickname}`, + ToastAndroid.SHORT + ); + }; + + return ( + ( + + )} /> + ); +}; diff --git a/src/components/UserList.tsx b/src/components/UserList.tsx new file mode 100644 index 00000000..a22cc2a6 --- /dev/null +++ b/src/components/UserList.tsx @@ -0,0 +1,75 @@ +import React, { useMemo, useState } from 'react'; +import { FlatList, StyleSheet, ToastAndroid } from 'react-native'; +import { ActivityIndicator, List, useTheme } from 'react-native-paper'; +import useSWRInfinite from 'swr/infinite'; +import { UserItem } from '.'; +import { useDebounce } from '../hook'; +import { Main } from '../types/searchUsers'; + +export const UserList = ({ searchQuery }: { searchQuery: string; }) => { + const appTheme = useTheme(); + + const [refreshing, setRefreshing] = useState(false); + const { data, setSize, error, mutate } = useSWRInfinite
( + (index) => `https://music.163.com/api/search/get/web?s=${searchQuery}&type=1002&offset=${index * 15}&limit=15`, + { suspense: true } + ); + + const users = useMemo(() => { + if (data && data?.[0].code === 200) { + return data?.map((item) => item.result.userprofiles).flat(); + } else { + ToastAndroid.show( + `Error: ${data?.[0].code} ${data?.[0].message}`, + ToastAndroid.SHORT + ); + return []; + } + }, [data]); + + const hasMore = useMemo(() => data && data[0].result && users.length !== data[0].result.userprofileCount, + [data, users]); + + const loadMore = useDebounce(() => { + if (hasMore) { + setSize(prev => prev + 1); + } + }, 1000); + + if (error) { + return ( + + ); + } + + return ( + } + keyExtractor={(item) => item.userId.toString()} + initialNumToRender={7} + onEndReachedThreshold={0.1} + onEndReached={loadMore} + onRefresh={async () => { + setRefreshing(true); + await mutate(); + setRefreshing(false); + }} + refreshing={refreshing} + ListFooterComponent={hasMore && users.length + ? : null} + ListEmptyComponent={} /> + ); +}; + +const styles = StyleSheet.create({ + footerLoading: { + marginVertical: '5%' + } +}); diff --git a/src/components/index.ts b/src/components/index.ts index 188797c8..64583a7b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -18,7 +18,6 @@ export * from './DialogWithRadioBtns'; export * from './DrawerItemList'; export * from './DrawerNavi'; export * from './FavToggle'; -export * from './TracksHeader'; export * from './FavsList'; export * from './ForwardButton'; export * from './HeaderCard'; @@ -43,6 +42,9 @@ export * from './TrackInfoBar'; export * from './TrackItem'; export * from './TrackListSheet'; export * from './TrackMenu'; +export * from './TracksHeader'; export * from './UserHeader'; +export * from './UserItem'; +export * from './UserList'; export * from './VersionItem'; diff --git a/src/pages/favs.tsx b/src/pages/favs.tsx index 7ea2f1c6..44a5754e 100644 --- a/src/pages/favs.tsx +++ b/src/pages/favs.tsx @@ -48,7 +48,7 @@ export function Favs() { ); } -export const styles = StyleSheet.create({ +const styles = StyleSheet.create({ appbar: { backgroundColor: 'transparent' }, diff --git a/src/pages/index.ts b/src/pages/index.ts index c5d46eaa..0fc5b951 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -2,6 +2,7 @@ export * from './album'; export * from './artist'; export * from './comments'; export * from './favs'; +export * from './login'; export * from './lyrics'; export * from './mv'; export * from './mvDetail'; diff --git a/src/pages/login.tsx b/src/pages/login.tsx new file mode 100644 index 00000000..5df3d4b6 --- /dev/null +++ b/src/pages/login.tsx @@ -0,0 +1,53 @@ +import React, { Suspense, useState } from 'react'; +import { StatusBar, StyleSheet } from 'react-native'; +import { ActivityIndicator, Searchbar } from 'react-native-paper'; +import { BlurBackground, UserList } from '../components'; +import { useAppSelector } from '../hook'; +import { selectUser } from '../redux/slices'; + +export const Login = () => { + const user = useAppSelector(selectUser); + + const [showQuery, setShowQuery] = useState(user.username); + const [searchQuery, setSearchQuery] = useState(user.username); + + const search = () => { + if (showQuery && searchQuery !== showQuery) { + setSearchQuery(showQuery); + } + }; + + return ( + + + + + }> + + + + ); +}; + +const styles = StyleSheet.create({ + searchbar: { + marginTop: StatusBar.currentHeight, + marginHorizontal: '2%', + backgroundColor: 'transparent' + }, + loading: { + marginTop: '20%' + }, +}); diff --git a/src/types/searchUsers.d.ts b/src/types/searchUsers.d.ts new file mode 100644 index 00000000..241cd2d9 --- /dev/null +++ b/src/types/searchUsers.d.ts @@ -0,0 +1,53 @@ +export interface Main { + result: Result; + code: number; + message: string; +} + +export interface Result { + userprofiles: Userprofile[]; + userprofileCount: number; +} + +export interface Userprofile { + defaultAvatar: boolean; + province: number; + authStatus: number; + followed: boolean; + avatarUrl: string; + accountStatus: number; + gender: number; + city: number; + birthday: number; + userId: number; + userType: number; + nickname: string; + signature: string; + description: string; + detailDescription: string; + avatarImgId: number; + backgroundImgId: number; + backgroundUrl: string; + authority: number; + mutual: boolean; + expertTags: null; + experts: null; + djStatus: number; + vipType: number; + remarkName: null; + authenticationTypes: number; + avatarDetail: null; + avatarImgIdStr: string; + backgroundImgIdStr: string; + anchor: boolean; + avatarImgId_str: string; + followeds: number; + follows: number; + alg: Alg; + playlistCount: number; + playlistBeSubscribedCount: number; +} + +export enum Alg { + AlgUserBasic = "alg_user_basic", +}