xmcp 7 years ago
commit
c9ccf351bf
  1. 3
      .gitignore
  2. 2444
      README.md
  3. 11232
      package-lock.json
  4. 17
      package.json
  5. 24
      public/index.html
  6. 30
      src/App.css
  7. 23
      src/App.js
  8. 12
      src/Common.css
  9. 31
      src/Common.js
  10. 25
      src/Flows.css
  11. 133
      src/Flows.js
  12. 19
      src/Title.css
  13. 22
      src/Title.js
  14. 21
      src/index.css
  15. 8
      src/index.js
  16. 117
      src/registerServiceWorker.js

3
.gitignore vendored

@ -0,0 +1,3 @@
/.idea/
node_modules/

2444
README.md

File diff suppressed because it is too large Load Diff

11232
package-lock.json generated

File diff suppressed because it is too large Load Diff

17
package.json

@ -0,0 +1,17 @@
{
"name": "webhole",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-scripts": "1.1.4",
"react-timeago": "^4.1.9"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

24
public/index.html

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>AsHole</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

30
src/App.css

@ -0,0 +1,30 @@
.box {
background-color: #fff;
border-radius: 5px;
margin: 1em 0;
padding: .5em;
box-shadow: 0 5px 20px #999;
}
.left-container .centered-line {
width: calc(100% - 2 * 50px);
}
.flow-item {
flex: 0 0 600px;
}
.flow-reply {
flex: 0 0 300px;
max-height: 15em;
overflow-y: hidden;
}
.left-container .centered-line,
.left-container .flow-item {
margin-left: 50px;
}
.flow-item-row {
display: flex;
overflow-x: hidden;
align-items: flex-start;
}

23
src/App.js

@ -0,0 +1,23 @@
import React, {Component} from 'react';
import './App.css';
import {Flow} from './Flows';
import {Title} from './Title';
class App extends Component {
show_details(info) {
}
render() {
return (
<div>
<Title />
<div className="left-container">
<Flow callback={(info)=>this.show_details(info)} mode="list" />
</div>
</div>
);
}
}
export default App;

12
src/Common.css

@ -0,0 +1,12 @@
.centered-line {
width: 100%;
text-align: center;
border-bottom: 1px solid #000;
line-height: 0.1em;
margin: 10px 0 20px;
}
.centered-line span {
background:#eee;
padding:0 10px;
}

31
src/Common.js

@ -0,0 +1,31 @@
import React, {Component} from 'react';
import TimeAgo from 'react-timeago';
import chineseStrings from 'react-timeago/lib/language-strings/zh-CN';
import buildFormatter from 'react-timeago/lib/formatters/buildFormatter';
import './Common.css';
const chinese_format=buildFormatter(chineseStrings);
function pad2(x) {
return x<10 ? '0'+x : ''+x;
}
export function Time(props) {
const time=new Date(props.stamp*1000);
return (
<span>
<TimeAgo date={time} formatter={chinese_format} />
&nbsp;
{time.getMonth()+1}-{time.getDate()}&nbsp;
{time.getHours()}:{pad2(time.getMinutes())}
</span>
);
}
export function CenteredLine(props) {
return (
<p className="centered-line">
<span>{props.text}</span>
</p>
)
}

25
src/Flows.css

@ -0,0 +1,25 @@
.flow-item-row img {
max-width: 100%;
}
.flow-item-row pre {
white-space: pre-wrap;
font-family: '微软雅黑', 'Microsoft YaHei', sans-serif;
}
.box-header-badge {
float: right;
border: 1px solid black;
border-radius: 7px;
margin: 0 .5em;
padding: 0 .5em;
}
.box-id {
font-family: Consolas, Courier, monospace;
opacity: .6;
}
.flow-reply-gray {
background-color: #e7e7e7;
}

133
src/Flows.js

@ -0,0 +1,133 @@
import React, {Component} from 'react';
import {Time, CenteredLine} from './Common.js';
import './Flows.css';
const IMAGE_BASE='http://www.pkuhelper.com/services/pkuhole/images/';
const AUDIO_BASE='http://www.pkuhelper.com/services/pkuhole/audios/';
function Reply(props) {
return (
<div className={'flow-reply box '+(props.info.islz ? '' : 'flow-reply-gray')}>
<div className="box-header">
<span className="box-id">#{props.info.cid}</span>&nbsp;
<Time stamp={props.info.timestamp} />
</div>
<pre>{props.info.text}</pre>
</div>
);
}
function ReplyPlaceholder(props) {
return (
<div className="box">
正在加载 {props.count} 条回复
</div>
);
}
class FlowChunkItem extends Component {
constructor(props) {
super(props);
this.state={
replies: [],
reply_loading: false,
};
this.info=props.info;
if(props.info.reply) {
this.state.reply_loading=true;
this.load_replies();
}
}
load_replies() {
fetch('http://www.pkuhelper.com:10301/pkuhelper/../services/pkuhole/api.php?action=getcomment&pid='+this.info.pid)
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0)
throw new Error(json.code);
this.setState({
replies: json.data,
reply_loading: false,
});
});
}
render() {
// props.do_show_details
return (
<div className="flow-item-row">
<div className="flow-item box">
<div className="box-header">
<span className="box-header-badge">{this.info.likenum} </span>
<span className="box-header-badge">{this.info.reply} 回复</span>
<span className="box-id">#{this.info.pid}</span>&nbsp;
<Time stamp={this.info.timestamp} />
</div>
<pre>{this.info.text}</pre>
{this.info.type==='image' ? <img src={IMAGE_BASE+this.info.url} /> : null}
{this.info.type==='audio' ? <audio src={AUDIO_BASE+this.info.url} /> : null}
</div>
{this.state.reply_loading && <ReplyPlaceholder count={this.info.reply} />}
{this.state.replies.map((reply)=><Reply info={reply} key={reply.cid} />)}
</div>
);
}
}
function FlowChunk(props) {
return (
<div className="flow-chunk">
<CenteredLine text={props.title} />
{props.list.map((info)=><FlowChunkItem key={info.pid} info={info} callback={props.callback} />)}
</div>
);
}
export class Flow extends Component {
constructor(props) {
super(props);
this.state={
mode: props.mode,
loaded_pages: 0,
chunks: []
};
this.load_page(1);
}
load_page(page) {
if(page>this.state.loaded_pages+1)
throw new Error('bad page');
if(page===this.state.loaded_pages+1) {
console.log('fetching page',page);
this.setState((prev,props)=>({
loaded_pages: prev.loaded_pages+1
}));
fetch('http://www.pkuhelper.com:10301/pkuhelper/../services/pkuhole/api.php?action=getlist&p='+page)
.then((res)=>res.json())
.then((json)=>{
if(json.code!==0)
throw new Error(json.code);
this.setState((prev,props)=>({
chunks: prev.chunks.concat([{
title: 'Page '+page,
data: json.data,
}])
}));
})
.catch((err)=>{
console.trace(err);
alert('load failed');
});
}
}
render() {
return (
<div className="flow-container">
{this.state.chunks.map((chunk)=>(
<FlowChunk title={chunk.title} list={chunk.data} key={chunk.title} callback={this.props.callback} />
))}
</div>
);
}
}

19
src/Title.css

@ -0,0 +1,19 @@
.title {
z-index: 1;
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 2em;
line-height: 2em;
font-size: 1.5em;
background-color: #fff;
padding: 0 .5em;
box-shadow: 0 0 25px #999;
margin-bottom: 1em;
}
.title-links {
float: right;
display: inline-block;
font-size: .7em;
}

22
src/Title.js

@ -0,0 +1,22 @@
import React, {Component} from 'react';
import './Title.css';
const tos=`P大树洞网页版
使用本网站时您需要了解并同意
- 所有数据来自 PKU Helper本站不对其内容负责
- 不接受关于修改 UI 的建议
- 英梨梨是我的你们都不要抢`;
export function Title(props) {
return (
<div className="title">
<div className="title-links">
<a onClick={()=>{alert(tos);}}>ToS</a>
<a href="https://github.com/xmcp/ashole">GitHub</a>
</div>
P大树洞
</div>
)
}

21
src/index.css

@ -0,0 +1,21 @@
body {
margin: 0;
padding: 0;
font-family: '微软雅黑', 'Microsoft YaHei', sans-serif;
background-color: #eee;
}
* {
box-sizing: border-box;
}
p {
margin: 0;
}
a {
text-decoration: none;
color: blue;
cursor: pointer;
margin: 0 .5em;
}

8
src/index.js

@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

117
src/registerServiceWorker.js

@ -0,0 +1,117 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
Loading…
Cancel
Save