Merge 0.0.1a #1

Merged
kdxcxs merged 24 commits from dev into master 4 years ago
  1. 24
      README.md
  2. 28
      android/app/src/main/AndroidManifest.xml
  3. 34
      package.json
  4. BIN
      screenshot.jpg
  5. 10
      src/App.js
  6. 88
      src/YooRouter.js
  7. 26
      src/api/HTTP.js
  8. 21
      src/api/StatusBarHeight.js
  9. 138
      src/component/YooForum.js
  10. 56
      src/component/YooForumTopic.js
  11. 59
      src/component/YooLogin.js
  12. 35
      src/component/YooSplash.js
  13. 6
      src/component/index.js
  14. 28
      src/ui/YooBackground.js
  15. 77
      src/ui/YooForumTopicUI.js
  16. 172
      src/ui/YooForumUI.js
  17. 63
      src/ui/YooLoginUI.js
  18. 28
      src/ui/YooReply.js
  19. 38
      src/ui/YooSplashUI.js
  20. 7475
      yarn.lock

24
README.md

@ -2,12 +2,32 @@
哟慕课 APP 是一个第三方的优慕课客户端 哟慕课 APP 是一个第三方的优慕课客户端
## 🎞背景 ![screenshot](./screenshot.jpg)
## 🎞 背景
无论是电脑版还是手机版的优慕课客户端用起来似乎都不是那么令人满意,电脑版在课程页面只能看到标题不能看到详情,手机版从详情页退出后会回到最顶端。于是就有了做一个第三方客户端的想法,也当是 React Native 的一次练手了 无论是电脑版还是手机版的优慕课客户端用起来似乎都不是那么令人满意,电脑版在课程页面只能看到标题不能看到详情,手机版从详情页退出后会回到最顶端。于是就有了做一个第三方客户端的想法,也当是 React Native 的一次练手了
## 📄TODO ## 🧨 版本说明
| 版本 | 说明 |
| ------ | ------------ |
| 0.0.1a | 首个可用版本 |
## 📄 TODO
- [ ] 弹出键盘不遮挡输入框 - [ ] 弹出键盘不遮挡输入框
- [ ] 根据像素比例设置登录背景图片大小 - [ ] 根据像素比例设置登录背景图片大小
- [ ] 添加应用图标 - [ ] 添加应用图标
- [ ] 适配 iOS 登录失败 toast
- [ ] 在启动时检测是否已登录
- [ ] Splash 检测网络出错后在 Splash UI 中显示错误并不跳转
- [ ] Forum 初始化失败后 UI 显示错误信息
- [ ] 当数据获取完成后通过动画转到话题列表
- [ ] 解决当动画进行时点击话题标题动画会被打断的问题
- [ ] 支持显示回复图片
- [ ] 在回复中显示头像
- [ ] 当滑动到页面底部时显示"加载更多"

28
android/app/src/main/AndroidManifest.xml

@ -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>

34
package.json

@ -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"
}
}

BIN
screenshot.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

10
src/App.js

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View, Platform} from 'react-native';
import {StatusBarHeight} from './api/StatusBarHeight'; import {StatusBarHeight} from './api/StatusBarHeight';
import YooLogin from './component/YooLogin'; import {YooBackground} from './component/index';
import {YooBackground} from './ui/YooBackground'; import YooRouter from './YooRouter';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
margin: 0, margin: 0,
padding: 0, padding: 0,
paddingTop: StatusBarHeight, paddingTop: Platform.OS === 'ios' ? StatusBarHeight : 0,
}, },
}); });
@ -16,7 +16,7 @@ export default function App() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<YooBackground /> <YooBackground />
<YooLogin /> <YooRouter />
</View> </View>
); );
} }

88
src/YooRouter.js

@ -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>
);
}
}

26
src/api/HTTP.js

@ -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};

21
src/api/StatusBarHeight.js

@ -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,
});

138
src/component/YooForum.js

@ -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} />;
}
}

56
src/component/YooForumTopic.js

@ -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}
/>
);
}
}

59
src/component/YooLogin.js

@ -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}
/>
);
} }
} }

35
src/component/YooSplash.js

@ -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 />;
}
}

6
src/component/index.js

@ -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};

28
src/ui/YooBackground.js

@ -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>
); );
} }

77
src/ui/YooForumTopicUI.js

@ -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>
);
}
}

172
src/ui/YooForumUI.js

@ -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>
);
}
}

63
src/ui/YooLoginUI.js

@ -6,6 +6,8 @@ import {
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
Text, Text,
ActivityIndicator,
ToastAndroid,
} from 'react-native'; } from 'react-native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -35,36 +37,61 @@ const styles = StyleSheet.create({
backgroundColor: '#1c90ce', backgroundColor: '#1c90ce',
width: 100, width: 100,
height: 50, height: 50,
borderRadius: 8, borderRadius: 25,
}, },
buttonText: { buttonText: {
fontWeight: 'bold', fontWeight: 'bold',
color: '#4d4c4d', color: 'mintcream',
fontSize: 26, fontSize: 26,
}, },
}); });
function LoginInput(props) {
return props.type === 'username' ? (
<TextInput style={styles.input} placeholder={'账号'} maxLength={10} />
) : (
<TextInput
style={styles.input}
placeholder={'密码'}
secureTextEntry={true}
/>
);
}
export default class YooLoginUI extends Component { export default class YooLoginUI extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
};
this.onButtonPress = this.onButtonPress.bind(this);
this.onLoginFailed = this.onLoginFailed.bind(this);
}
onButtonPress() {
if (!this.state.loading) {
this.setState({loading: true});
this.props.loginCallback(this.onLoginFailed);
}
}
onLoginFailed(hint) {
this.setState({loading: false});
ToastAndroid.show(hint, ToastAndroid.SHORT);
}
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Image source={require('../../assets/icon.png')} style={styles.logo} /> <Image source={require('../../assets/icon.png')} style={styles.logo} />
<LoginInput type={'username'} /> <TextInput
<LoginInput type={'password'} /> style={styles.input}
<TouchableOpacity style={styles.button}> placeholder={'账号'}
<Text style={styles.buttonText}>登录</Text> maxLength={10}
value={this.props.username}
onChangeText={(username) => this.props.setUsername(username)}
/>
<TextInput
style={styles.input}
placeholder={'密码'}
secureTextEntry={true}
value={this.props.password}
onChangeText={(password) => this.props.setPassword(password)}
/>
<TouchableOpacity style={styles.button} onPress={this.onButtonPress}>
{this.state.loading ? (
<ActivityIndicator size="large" color="#00ff00" />
) : (
<Text style={styles.buttonText}>登录</Text>
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );

28
src/ui/YooReply.js

@ -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>
);
}
}

38
src/ui/YooSplashUI.js

@ -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>
);
}
}

7475
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save