20 changed files with 8374 additions and 34 deletions
@ -0,0 +1,28 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
package="com.yoomoocrn"> |
||||
|
|
||||
|
<uses-permission android:name="android.permission.INTERNET" /> |
||||
|
|
||||
|
<application |
||||
|
android:name=".MainApplication" |
||||
|
android:label="@string/app_name" |
||||
|
android:icon="@mipmap/ic_launcher" |
||||
|
android:roundIcon="@mipmap/ic_launcher_round" |
||||
|
android:allowBackup="false" |
||||
|
android:theme="@style/AppTheme"> |
||||
|
<activity |
||||
|
android:name=".MainActivity" |
||||
|
android:label="@string/app_name" |
||||
|
android:screenOrientation="portrait" |
||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" |
||||
|
android:launchMode="singleTask" |
||||
|
android:windowSoftInputMode="adjustResize"> |
||||
|
<intent-filter> |
||||
|
<action android:name="android.intent.action.MAIN" /> |
||||
|
<category android:name="android.intent.category.LAUNCHER" /> |
||||
|
</intent-filter> |
||||
|
</activity> |
||||
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> |
||||
|
</application> |
||||
|
|
||||
|
</manifest> |
@ -0,0 +1,34 @@ |
|||||
|
{ |
||||
|
"name": "YoomoocRN", |
||||
|
"version": "0.0.1", |
||||
|
"private": true, |
||||
|
"scripts": { |
||||
|
"android": "react-native run-android", |
||||
|
"ios": "react-native run-ios", |
||||
|
"start": "react-native start", |
||||
|
"test": "jest", |
||||
|
"lint": "eslint ." |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@react-native-async-storage/async-storage": "^1.13.2", |
||||
|
"@react-native-community/cookies": "^5.0.1", |
||||
|
"buffer": "^6.0.3", |
||||
|
"iconv-lite": "^0.6.2", |
||||
|
"react": "16.13.1", |
||||
|
"react-native": "0.63.3", |
||||
|
"stream": "^0.0.2" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@babel/core": "^7.12.9", |
||||
|
"@babel/runtime": "^7.12.5", |
||||
|
"@react-native-community/eslint-config": "^2.0.0", |
||||
|
"babel-jest": "^26.6.3", |
||||
|
"eslint": "^7.14.0", |
||||
|
"jest": "^26.6.3", |
||||
|
"metro-react-native-babel-preset": "^0.64.0", |
||||
|
"react-test-renderer": "16.13.1" |
||||
|
}, |
||||
|
"jest": { |
||||
|
"preset": "react-native" |
||||
|
} |
||||
|
} |
After Width: | Height: | Size: 451 KiB |
@ -0,0 +1,88 @@ |
|||||
|
import React, {Component} from 'react'; |
||||
|
import {View, Animated, Dimensions} from 'react-native'; |
||||
|
import {YooLogin, YooForum, YooSplash} from './component/index'; |
||||
|
|
||||
|
const width = Dimensions.get('window').width; |
||||
|
|
||||
|
export default class YooRouter extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
currentPage: 'splash', |
||||
|
splashX: new Animated.Value(0), |
||||
|
loginX: new Animated.Value(width), |
||||
|
forumX: new Animated.Value(width), |
||||
|
}; |
||||
|
this.gotoForum = this.gotoForum.bind(this); |
||||
|
this.gotoLogin = this.gotoLogin.bind(this); |
||||
|
this.bindForumInitializer = this.bindForumInitializer.bind(this); |
||||
|
this.initForum = null; |
||||
|
} |
||||
|
|
||||
|
bindForumInitializer(forumRef) { |
||||
|
this.initForum = forumRef.initForum; |
||||
|
} |
||||
|
|
||||
|
gotoLogin() { |
||||
|
this.setState({currentPage: 'login'}); |
||||
|
Animated.parallel([ |
||||
|
Animated.timing(this.state.splashX, { |
||||
|
toValue: -width, |
||||
|
duration: 250, |
||||
|
useNativeDriver: false, |
||||
|
}), |
||||
|
Animated.timing(this.state.loginX, { |
||||
|
toValue: 0, |
||||
|
duration: 250, |
||||
|
useNativeDriver: false, |
||||
|
}), |
||||
|
]).start(); |
||||
|
} |
||||
|
|
||||
|
gotoForum() { |
||||
|
this.initForum(); |
||||
|
if (this.state.currentPage === 'splash') { |
||||
|
Animated.parallel([ |
||||
|
Animated.timing(this.state.splashX, { |
||||
|
toValue: -width, |
||||
|
duration: 250, |
||||
|
useNativeDriver: false, |
||||
|
}), |
||||
|
Animated.timing(this.state.forumX, { |
||||
|
toValue: 0, |
||||
|
duration: 250, |
||||
|
useNativeDriver: false, |
||||
|
}), |
||||
|
]).start(); |
||||
|
} else { |
||||
|
Animated.parallel([ |
||||
|
Animated.timing(this.state.loginX, { |
||||
|
toValue: -width, |
||||
|
duration: 250, |
||||
|
useNativeDriver: false, |
||||
|
}), |
||||
|
Animated.timing(this.state.forumX, { |
||||
|
toValue: 0, |
||||
|
duration: 250, |
||||
|
useNativeDriver: false, |
||||
|
}), |
||||
|
]).start(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<View> |
||||
|
<Animated.View style={{left: this.state.splashX}}> |
||||
|
<YooSplash gotoLogin={this.gotoLogin} gotoForum={this.gotoForum} /> |
||||
|
</Animated.View> |
||||
|
<Animated.View style={{left: this.state.loginX}}> |
||||
|
<YooLogin gotoForum={this.gotoForum} /> |
||||
|
</Animated.View> |
||||
|
<Animated.View style={{left: this.state.forumX}}> |
||||
|
<YooForum bindInitializer={this.bindForumInitializer} /> |
||||
|
</Animated.View> |
||||
|
</View> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
import iconv from 'iconv-lite'; |
||||
|
import {Buffer} from 'buffer'; |
||||
|
|
||||
|
function gbkFetch(method, url, data) { |
||||
|
return new Promise(function (resolve, reject) { |
||||
|
const request = new XMLHttpRequest(); |
||||
|
|
||||
|
request.onload = () => { |
||||
|
if (request.status === 200) { |
||||
|
resolve(iconv.decode(Buffer.from(request.response), 'gbk')); |
||||
|
} else { |
||||
|
reject(new Error(request.statusText)); |
||||
|
} |
||||
|
}; |
||||
|
request.onerror = () => reject(new Error(request.statusText)); |
||||
|
request.responseType = 'arraybuffer'; |
||||
|
|
||||
|
request.open(method, url); |
||||
|
for (let headerKey in data.headers) { |
||||
|
request.setRequestHeader(headerKey, data.headers[headerKey]); |
||||
|
} |
||||
|
request.send(data.body); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export {gbkFetch}; |
@ -0,0 +1,21 @@ |
|||||
|
import {Dimensions, Platform, StatusBar} from 'react-native'; |
||||
|
|
||||
|
const X_WIDTH = 375; |
||||
|
const X_HEIGHT = 812; |
||||
|
|
||||
|
const XSMAX_WIDTH = 414; |
||||
|
const XSMAX_HEIGHT = 896; |
||||
|
|
||||
|
const {height, width} = Dimensions.get('window'); |
||||
|
|
||||
|
export const isIPhoneX = () => |
||||
|
Platform.OS === 'ios' && !Platform.isPad && !Platform.isTVOS |
||||
|
? (width === X_WIDTH && height === X_HEIGHT) || |
||||
|
(width === XSMAX_WIDTH && height === XSMAX_HEIGHT) |
||||
|
: false; |
||||
|
|
||||
|
export const StatusBarHeight = Platform.select({ |
||||
|
ios: isIPhoneX() ? 44 : 20, |
||||
|
android: StatusBar.currentHeight, |
||||
|
default: 0, |
||||
|
}); |
@ -0,0 +1,138 @@ |
|||||
|
import React, {Component} from 'react'; |
||||
|
import {ToastAndroid} from 'react-native'; |
||||
|
import YooForumUI from '../ui/YooForumUI'; |
||||
|
import {gbkFetch} from '../api/HTTP'; |
||||
|
import CookieManager from '@react-native-community/cookies'; |
||||
|
import * as cheerio from 'cheerio'; |
||||
|
|
||||
|
const ShowErrorToast = () => |
||||
|
ToastAndroid.show('获取数据时出错', ToastAndroid.SHORT); |
||||
|
|
||||
|
export default class YooForum extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
hint: '准备中', |
||||
|
topics: [], |
||||
|
}; |
||||
|
this.initForum = this.initForum.bind(this); |
||||
|
this.getTopicList = this.getTopicList.bind(this); |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.props.bindInitializer(this); |
||||
|
} |
||||
|
|
||||
|
initForum() { |
||||
|
// get dwr session id
|
||||
|
gbkFetch( |
||||
|
'POST', |
||||
|
'http://eol.ctbu.edu.cn/meol/dwr/call/plaincall/__System.generateId.dwr', |
||||
|
{ |
||||
|
headers: { |
||||
|
Origin: 'http://eol.ctbu.edu.cn', |
||||
|
'Content-Type': 'text/plain', |
||||
|
'User-Agent': 'YooMooc', |
||||
|
Referer: |
||||
|
'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/index.jsp?courseId=46445', |
||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7', |
||||
|
}, |
||||
|
body: |
||||
|
'callCount=1\n' + |
||||
|
'c0-scriptName=__System\n' + |
||||
|
'c0-methodName=generateId\n' + |
||||
|
'c0-id=0\n' + |
||||
|
'batchId=0\n' + |
||||
|
'instanceId=0\n' + |
||||
|
'page=%2Fmeol%2Fjpk%2Fcourse%2Flayout%2Fnewpage%2Findex.jsp%3FcourseId%3D46445\n' + |
||||
|
'scriptSessionId=\n' + |
||||
|
'windowName=\n', |
||||
|
}, |
||||
|
) |
||||
|
.then((response) => { |
||||
|
if (/[^"]*"\);/.test(response)) { |
||||
|
const exDate = new Date(); |
||||
|
exDate.setDate(exDate.getDate() + 1); |
||||
|
CookieManager.set( |
||||
|
'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/index.jsp?courseId=46445', |
||||
|
{ |
||||
|
name: 'DWRSESSIONID', |
||||
|
value: response.match(/[^"]*"\);/)[0].split('"')[0], |
||||
|
domain: 'eol.ctbu.edu.cn', |
||||
|
path: '/meol', |
||||
|
version: '1', |
||||
|
expires: exDate.toUTCString(), |
||||
|
}, |
||||
|
).then(() => { |
||||
|
// it is needed to request some pages before getting the topic list
|
||||
|
// maybe the server is judging which course the user is
|
||||
|
gbkFetch( |
||||
|
'GET', |
||||
|
'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/index.jsp?courseId=46445', |
||||
|
{ |
||||
|
headers: { |
||||
|
'Upgrade-Insecure-Requests': '1', |
||||
|
'User-Agent': 'YooMooc', |
||||
|
}, |
||||
|
}, |
||||
|
) |
||||
|
.then(() => { |
||||
|
gbkFetch( |
||||
|
'GET', |
||||
|
'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/default_demonstrate.jsp', |
||||
|
{ |
||||
|
'Upgrade-Insecure-Requests': '1', |
||||
|
'User-Agent': 'YooMooc', |
||||
|
Referer: |
||||
|
'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/index.jsp?courseId=46445', |
||||
|
}, |
||||
|
) |
||||
|
.then(() => { |
||||
|
this.getTopicList(); |
||||
|
}) |
||||
|
.catch(ShowErrorToast); |
||||
|
}) |
||||
|
.catch(ShowErrorToast); |
||||
|
}); |
||||
|
} else { |
||||
|
ShowErrorToast(); |
||||
|
} |
||||
|
}) |
||||
|
.catch(ShowErrorToast); |
||||
|
} |
||||
|
|
||||
|
getTopicList(page = 1) { |
||||
|
this.setState({hint: '获取数据中'}); |
||||
|
gbkFetch( |
||||
|
'GET', |
||||
|
`http://eol.ctbu.edu.cn/meol/common/faq/forum.jsp?viewtype=thread&forumid=102211&cateId=0&s_gotopage=${page}`, |
||||
|
{ |
||||
|
headers: { |
||||
|
'Upgrade-Insecure-Requests': '1', |
||||
|
'User-Agent': 'YooMooc', |
||||
|
Referer: |
||||
|
'http://eol.ctbu.edu.cn/meol/common/faq/forum.jsp?count=MODITIME&forumid=102211', |
||||
|
}, |
||||
|
}, |
||||
|
).then((response) => { |
||||
|
const newTopics = []; |
||||
|
const appendNewTopic = (threadID, title, owner) => { |
||||
|
newTopics.push({threadID, title, owner}); |
||||
|
}; |
||||
|
const $ = cheerio.load(response, {ignoreWhitespace: true}); |
||||
|
$('td.left').each(function () { |
||||
|
const current = $(this); |
||||
|
appendNewTopic( |
||||
|
current.children('a').attr('href').slice(20), |
||||
|
current.text(), |
||||
|
current.next().text(), |
||||
|
); |
||||
|
}); |
||||
|
this.setState({topics: [...this.state.topics, ...newTopics], hint: ''}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return <YooForumUI topics={this.state.topics} hint={this.state.hint} />; |
||||
|
} |
||||
|
} |
@ -0,0 +1,56 @@ |
|||||
|
import React, {Component} from 'react'; |
||||
|
import YooForumTopicUI from '../ui/YooForumTopicUI'; |
||||
|
import {gbkFetch} from '../api/HTTP'; |
||||
|
import * as cheerio from 'cheerio'; |
||||
|
|
||||
|
export default class YooForumTopic extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
replies: [], |
||||
|
}; |
||||
|
this.getReplies = this.getReplies.bind(this); |
||||
|
} |
||||
|
|
||||
|
componentDidMount() { |
||||
|
this.getReplies(); |
||||
|
} |
||||
|
|
||||
|
getReplies() { |
||||
|
gbkFetch( |
||||
|
'GET', |
||||
|
`http://eol.ctbu.edu.cn/meol/common/faq/thread.jsp?threadid=${this.props.topic.threadID}`, |
||||
|
{ |
||||
|
headers: 'YooMooc', |
||||
|
}, |
||||
|
).then((response) => { |
||||
|
const newReplies = []; |
||||
|
const appendReply = (username, content) => { |
||||
|
newReplies.push({username, content}); |
||||
|
}; |
||||
|
const $ = cheerio.load(response, {ignoreWhitespace: true}); |
||||
|
$('input[type=hidden]').each(function () { |
||||
|
const current = $(this); |
||||
|
const currentUserInfo = current.parent().parent().parent().parent(); |
||||
|
if (currentUserInfo.html().slice(2, 4) === 'td') { |
||||
|
appendReply( |
||||
|
currentUserInfo.find('h6').text().split(' ')[1], |
||||
|
cheerio.load(current.attr('value')).text(), |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
this.setState({replies: [...this.state.replies, ...newReplies]}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<YooForumTopicUI |
||||
|
topic={this.props.topic} |
||||
|
showDetail={this.props.showDetail} |
||||
|
hideDetail={this.props.hideDetail} |
||||
|
replies={this.state.replies} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -1,12 +1,69 @@ |
|||||
import React, {Component} from 'react'; |
import React, {Component} from 'react'; |
||||
import YooLoginUI from '../ui/YooLoginUI'; |
import YooLoginUI from '../ui/YooLoginUI'; |
||||
|
import CookieManager from '@react-native-community/cookies'; |
||||
|
import {gbkFetch} from '../api/HTTP'; |
||||
|
|
||||
export default class YooLogin extends Component { |
export default class YooLogin extends Component { |
||||
constructor(props) { |
constructor(props) { |
||||
super(props); |
super(props); |
||||
|
this.state = { |
||||
|
username: '', |
||||
|
password: '', |
||||
|
}; |
||||
|
this.setUsername = this.setUsername.bind(this); |
||||
|
this.setPassword = this.setPassword.bind(this); |
||||
|
this.login = this.login.bind(this); |
||||
|
} |
||||
|
|
||||
|
setUsername(value) { |
||||
|
this.setState({username: value}); |
||||
|
} |
||||
|
|
||||
|
setPassword(value) { |
||||
|
this.setState({password: value}); |
||||
|
} |
||||
|
|
||||
|
login(onFail) { |
||||
|
gbkFetch('POST', 'http://eol.ctbu.edu.cn/meol/loginCheck.do', { |
||||
|
headers: { |
||||
|
'Cache-Control': 'max-age=0', |
||||
|
'Upgrade-Insecure-Requests': '1', |
||||
|
Origin: 'http://eol.ctbu.edu.cn', |
||||
|
'Content-Type': 'application/x-www-form-urlencoded', |
||||
|
'User-Agent': 'YooMooc', |
||||
|
Referer: |
||||
|
'http://eol.ctbu.edu.cn/meol/common/security/login.jsp?enterLid=46445', |
||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7', |
||||
|
}, |
||||
|
body: |
||||
|
'logintoken=' + |
||||
|
new Date().getTime() + |
||||
|
'&enterLid=46445' + |
||||
|
'&IPT_LOGINUSERNAME=' + |
||||
|
this.state.username + |
||||
|
'&IPT_LOGINPASSWORD=' + |
||||
|
this.state.password, |
||||
|
}) |
||||
|
.then((response) => { |
||||
|
if (/<title>(.|\n)*用户登录(.|\n)*<\/title>/.test(response)) { |
||||
|
onFail('用户名或密码错误'); |
||||
|
CookieManager.clearAll(); |
||||
|
} else if (/<title>(.|\n)*网络课程(.|\n)*<\/title>/.test(response)) { |
||||
|
this.props.gotoForum(); |
||||
|
} |
||||
|
}) |
||||
|
.catch(() => onFail('登陆失败')); |
||||
} |
} |
||||
|
|
||||
render() { |
render() { |
||||
return <YooLoginUI />; |
return ( |
||||
|
<YooLoginUI |
||||
|
username={this.state.username} |
||||
|
password={this.state.password} |
||||
|
setUsername={this.setUsername} |
||||
|
setPassword={this.setPassword} |
||||
|
loginCallback={this.login} |
||||
|
/> |
||||
|
); |
||||
} |
} |
||||
} |
} |
||||
|
@ -0,0 +1,35 @@ |
|||||
|
import React, {Component} from 'react'; |
||||
|
import {ToastAndroid} from 'react-native'; |
||||
|
import YooSplashUI from '../ui/YooSplashUI'; |
||||
|
import {gbkFetch} from '../api/HTTP'; |
||||
|
import CookieManager from '@react-native-community/cookies'; |
||||
|
|
||||
|
export default class YooSplash extends Component { |
||||
|
componentDidMount() { |
||||
|
gbkFetch('POST', 'http://eol.ctbu.edu.cn/meol/loginCheck.do', { |
||||
|
headers: { |
||||
|
'Cache-Control': 'max-age=0', |
||||
|
'Upgrade-Insecure-Requests': '1', |
||||
|
Origin: 'http://eol.ctbu.edu.cn', |
||||
|
'Content-Type': 'application/x-www-form-urlencoded', |
||||
|
'User-Agent': 'YooMooc', |
||||
|
Referer: |
||||
|
'http://eol.ctbu.edu.cn/meol/common/security/login.jsp?enterLid=46445', |
||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7', |
||||
|
}, |
||||
|
}) |
||||
|
.then((response) => { |
||||
|
if (/<title>(.|\n)*用户登录(.|\n)*<\/title>/.test(response)) { |
||||
|
this.props.gotoLogin(); |
||||
|
CookieManager.clearAll(); |
||||
|
} else if (/<title>(.|\n)*网络课程(.|\n)*<\/title>/.test(response)) { |
||||
|
this.props.gotoForum(); |
||||
|
} |
||||
|
}) |
||||
|
.catch(() => ToastAndroid.show('无法连接到服务器', ToastAndroid.SHORT)); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return <YooSplashUI />; |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
import YooLogin from './YooLogin'; |
||||
|
import YooForum from './YooForum'; |
||||
|
import YooBackground from '../ui/YooBackground'; |
||||
|
import YooSplash from './YooSplash'; |
||||
|
|
||||
|
export {YooLogin, YooForum, YooBackground, YooSplash}; |
@ -1,20 +1,34 @@ |
|||||
import React from 'react'; |
import React, {useState} from 'react'; |
||||
import {View, Image, StyleSheet} from 'react-native'; |
import {Image, StyleSheet, Animated} from 'react-native'; |
||||
|
|
||||
export function YooBackground() { |
export default function YooBackground() { |
||||
|
const imgPosition = useState(new Animated.ValueXY())[0]; |
||||
const styles = StyleSheet.create({ |
const styles = StyleSheet.create({ |
||||
backgroundContainer: { |
backgroundContainer: { |
||||
width: '100%', |
transform: [{translateX: -1500}], |
||||
height: '100%', |
|
||||
position: 'absolute', |
position: 'absolute', |
||||
zIndex: -1, |
zIndex: -1, |
||||
}, |
}, |
||||
}); |
}); |
||||
|
setInterval(() => { |
||||
|
Animated.timing(imgPosition, { |
||||
|
toValue: { |
||||
|
x: -Math.floor(Math.random() * 5000), |
||||
|
y: -Math.floor(Math.random() * 3000), |
||||
|
}, |
||||
|
duration: 10000, |
||||
|
useNativeDriver: true, |
||||
|
}).start(); |
||||
|
}, 10000); |
||||
return ( |
return ( |
||||
<View style={styles.backgroundContainer}> |
<Animated.View |
||||
|
style={[ |
||||
|
styles.backgroundContainer, |
||||
|
{transform: imgPosition.getTranslateTransform()}, |
||||
|
]}> |
||||
<Image |
<Image |
||||
source={require('../../assets/pawel-czerwinski-aelD0Zrmsy0-unsplash.jpg')} |
source={require('../../assets/pawel-czerwinski-aelD0Zrmsy0-unsplash.jpg')} |
||||
/> |
/> |
||||
</View> |
</Animated.View> |
||||
); |
); |
||||
} |
} |
||||
|
@ -0,0 +1,77 @@ |
|||||
|
import React, {Component, createRef} from 'react'; |
||||
|
import { |
||||
|
Animated, |
||||
|
View, |
||||
|
Text, |
||||
|
StyleSheet, |
||||
|
Pressable, |
||||
|
Dimensions, |
||||
|
} from 'react-native'; |
||||
|
|
||||
|
const screenWidth = Dimensions.get('window').width; |
||||
|
const styles = StyleSheet.create({ |
||||
|
container: { |
||||
|
backgroundColor: 'mintcream', |
||||
|
borderRadius: 8, |
||||
|
padding: 16, |
||||
|
margin: 8, |
||||
|
width: screenWidth - 16, |
||||
|
}, |
||||
|
title: { |
||||
|
fontSize: 24, |
||||
|
}, |
||||
|
owner: { |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
}); |
||||
|
const AnimatedPressable = Animated.createAnimatedComponent(Pressable); |
||||
|
|
||||
|
export default class YooForumTopicUI extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
detailShowing: false, |
||||
|
layoutY: 0, |
||||
|
translate: new Animated.ValueXY({x: 0, y: 0}), |
||||
|
}; |
||||
|
this.topicHeaderRef = createRef(); |
||||
|
this.onPress = this.onPress.bind(this); |
||||
|
this.onAnimationFinished = this.onAnimationFinished.bind(this); |
||||
|
} |
||||
|
|
||||
|
onAnimationFinished() { |
||||
|
this.setState({detailShowing: !this.state.detailShowing}); |
||||
|
} |
||||
|
|
||||
|
onPress() { |
||||
|
if (!this.state.detailShowing) { |
||||
|
this.props.showDetail( |
||||
|
this.state.translate, |
||||
|
this.state.layoutY, |
||||
|
this.onAnimationFinished, |
||||
|
this.props.replies, |
||||
|
); |
||||
|
} else { |
||||
|
this.props.hideDetail(this.state.translate, this.onAnimationFinished); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<AnimatedPressable |
||||
|
style={[ |
||||
|
styles.container, |
||||
|
{transform: this.state.translate.getTranslateTransform()}, |
||||
|
]} |
||||
|
onPress={this.onPress} |
||||
|
onLayout={(event) => { |
||||
|
this.setState({layoutY: event.nativeEvent.layout.y}); |
||||
|
}}> |
||||
|
<View ref={this.topicHeaderRef}> |
||||
|
<Text style={styles.owner}>{this.props.topic.owner}</Text> |
||||
|
<Text style={styles.title}>{this.props.topic.title}</Text> |
||||
|
</View> |
||||
|
</AnimatedPressable> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,172 @@ |
|||||
|
import React, {Component, createRef} from 'react'; |
||||
|
import { |
||||
|
View, |
||||
|
ScrollView, |
||||
|
Text, |
||||
|
StyleSheet, |
||||
|
ActivityIndicator, |
||||
|
Image, |
||||
|
Animated, |
||||
|
Dimensions, |
||||
|
} from 'react-native'; |
||||
|
import YooForumTopic from '../component/YooForumTopic'; |
||||
|
import YooReply from './YooReply'; |
||||
|
|
||||
|
const screenWidth = Dimensions.get('window').width; |
||||
|
const screenHeight = Dimensions.get('window').height; |
||||
|
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); |
||||
|
const styles = StyleSheet.create({ |
||||
|
container: { |
||||
|
width: screenWidth * 2, |
||||
|
}, |
||||
|
repliesContainer: { |
||||
|
position: 'absolute', |
||||
|
left: screenWidth, |
||||
|
width: screenWidth, |
||||
|
height: screenHeight - 124, |
||||
|
}, |
||||
|
hintContainer: { |
||||
|
flex: 1, |
||||
|
alignItems: 'center', |
||||
|
left: -screenWidth / 2, |
||||
|
}, |
||||
|
logo: { |
||||
|
marginTop: 120, |
||||
|
width: 200, |
||||
|
height: 200, |
||||
|
marginBottom: 16, |
||||
|
}, |
||||
|
hint: { |
||||
|
fontSize: 24, |
||||
|
fontWeight: 'bold', |
||||
|
}, |
||||
|
indicator: { |
||||
|
marginTop: 64, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default class YooForumUI extends Component { |
||||
|
constructor(props) { |
||||
|
super(props); |
||||
|
this.state = { |
||||
|
scrollEnabled: true, |
||||
|
currentPosition: 0, |
||||
|
translateX: new Animated.Value(0), |
||||
|
currentReplies: [], |
||||
|
replyTranslateY: new Animated.Value(0), |
||||
|
}; |
||||
|
this.replyRef = createRef(); |
||||
|
this.showDetail = this.showDetail.bind(this); |
||||
|
this.hideDetail = this.hideDetail.bind(this); |
||||
|
} |
||||
|
|
||||
|
showDetail(translate, layoutY, onAnimationFinished, replies) { |
||||
|
this.replyRef.current.scrollTo({x: 0, y: 0, animated: false}); |
||||
|
this.setState({ |
||||
|
scrollEnabled: false, |
||||
|
currentReplies: replies, |
||||
|
}); |
||||
|
this.state.replyTranslateY.setValue( |
||||
|
this.state.currentPosition + 100 + screenHeight, |
||||
|
); |
||||
|
Animated.parallel([ |
||||
|
Animated.timing(translate, { |
||||
|
toValue: { |
||||
|
x: screenWidth, |
||||
|
y: this.state.currentPosition - layoutY + 8, |
||||
|
}, |
||||
|
duration: 250, |
||||
|
useNativeDriver: true, |
||||
|
}), |
||||
|
Animated.timing(this.state.translateX, { |
||||
|
toValue: -screenWidth, |
||||
|
duration: 250, |
||||
|
useNativeDriver: true, |
||||
|
}), |
||||
|
Animated.timing(this.state.replyTranslateY, { |
||||
|
toValue: this.state.currentPosition + 100, |
||||
|
duration: 250, |
||||
|
delay: 250, |
||||
|
useNativeDriver: true, |
||||
|
}), |
||||
|
]).start(onAnimationFinished); |
||||
|
} |
||||
|
|
||||
|
hideDetail(translate, onAnimationFinished) { |
||||
|
this.setState({scrollEnabled: true}); |
||||
|
Animated.parallel([ |
||||
|
Animated.timing(translate, { |
||||
|
toValue: { |
||||
|
x: 0, |
||||
|
y: 0, |
||||
|
}, |
||||
|
duration: 250, |
||||
|
useNativeDriver: true, |
||||
|
}), |
||||
|
Animated.timing(this.state.translateX, { |
||||
|
toValue: 0, |
||||
|
duration: 250, |
||||
|
useNativeDriver: true, |
||||
|
}), |
||||
|
Animated.timing(this.state.replyTranslateY, { |
||||
|
toValue: this.state.currentPosition + 100 + screenHeight, |
||||
|
duration: 250, |
||||
|
useNativeDriver: true, |
||||
|
}), |
||||
|
]).start(onAnimationFinished); |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
return ( |
||||
|
<AnimatedScrollView |
||||
|
style={[ |
||||
|
styles.container, |
||||
|
{ |
||||
|
transform: [ |
||||
|
{ |
||||
|
translateX: this.state.translateX, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]} |
||||
|
scrollEnabled={this.state.scrollEnabled} |
||||
|
onScroll={(event) => |
||||
|
this.setState({currentPosition: event.nativeEvent.contentOffset.y}) |
||||
|
}> |
||||
|
{this.props.hint === '' ? ( |
||||
|
this.props.topics.map((topic, key) => ( |
||||
|
<YooForumTopic |
||||
|
key={key} |
||||
|
topic={topic} |
||||
|
showDetail={this.showDetail} |
||||
|
hideDetail={this.hideDetail} |
||||
|
/> |
||||
|
)) |
||||
|
) : ( |
||||
|
<View style={styles.hintContainer}> |
||||
|
<Image |
||||
|
source={require('../../assets/icon.png')} |
||||
|
style={styles.logo} |
||||
|
/> |
||||
|
<Text style={styles.hint}>{this.props.hint}</Text> |
||||
|
<ActivityIndicator |
||||
|
size="large" |
||||
|
color="#0000ff" |
||||
|
style={styles.indicator} |
||||
|
/> |
||||
|
</View> |
||||
|
)} |
||||
|
<AnimatedScrollView |
||||
|
ref={this.replyRef} |
||||
|
style={[ |
||||
|
styles.repliesContainer, |
||||
|
{transform: [{translateY: this.state.replyTranslateY}]}, |
||||
|
]}> |
||||
|
{this.state.currentReplies.map((reply, key) => ( |
||||
|
<YooReply key={key} reply={reply} /> |
||||
|
))} |
||||
|
</AnimatedScrollView> |
||||
|
</AnimatedScrollView> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
import React, {Component} from 'react'; |
||||
|
import {View, Text, StyleSheet} from 'react-native'; |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
container: { |
||||
|
backgroundColor: 'lightgray', |
||||
|
borderRadius: 8, |
||||
|
padding: 16, |
||||
|
margin: 8, |
||||
|
}, |
||||
|
username: { |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
content: { |
||||
|
fontSize: 24, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default class YooReply extends Component { |
||||
|
render() { |
||||
|
return ( |
||||
|
<View style={styles.container}> |
||||
|
<Text style={styles.username}>{this.props.reply.username}</Text> |
||||
|
<Text style={styles.content}>{this.props.reply.content}</Text> |
||||
|
</View> |
||||
|
); |
||||
|
} |
||||
|
} |
@ -0,0 +1,38 @@ |
|||||
|
import React, {Component} from 'react'; |
||||
|
import {View, StyleSheet, Image, Text, ActivityIndicator} from 'react-native'; |
||||
|
|
||||
|
const styles = StyleSheet.create({ |
||||
|
container: { |
||||
|
flex: 1, |
||||
|
alignItems: 'center', |
||||
|
}, |
||||
|
logo: { |
||||
|
marginTop: 120, |
||||
|
width: 200, |
||||
|
height: 200, |
||||
|
marginBottom: 16, |
||||
|
}, |
||||
|
text: { |
||||
|
fontSize: 24, |
||||
|
fontWeight: 'bold', |
||||
|
}, |
||||
|
indicator: { |
||||
|
marginTop: 64, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default class YooSplashUI extends Component { |
||||
|
render() { |
||||
|
return ( |
||||
|
<View style={styles.container}> |
||||
|
<Image source={require('../../assets/icon.png')} style={styles.logo} /> |
||||
|
<Text style={styles.text}>正在检查网络,请稍后</Text> |
||||
|
<ActivityIndicator |
||||
|
size="large" |
||||
|
color="#0000ff" |
||||
|
style={styles.indicator} |
||||
|
/> |
||||
|
</View> |
||||
|
); |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
Loading…
Reference in new issue