forked from newthuhole/hole_thu_frontend
format code
This commit is contained in:
55
.eslintrc
Normal file
55
.eslintrc
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/react"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly",
|
||||
"React": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"react"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"src/infrastructure/",
|
||||
"src/react-lazyload/"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "warn",
|
||||
"react/jsx-indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"indentLogicalExpressions": true
|
||||
}
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"args": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
7
.prettierrc.js
Normal file
7
.prettierrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
trailingComma: 'all',
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
endOfLine: 'auto',
|
||||
}
|
||||
74
src/App.js
74
src/App.js
@@ -11,16 +11,18 @@ import {LoginPopup, TitleLine} from './infrastructure/widgets';
|
||||
const MAX_SIDEBAR_STACK_SIZE = 10;
|
||||
|
||||
function DeprecatedAlert(props) {
|
||||
return (
|
||||
<div id="global-hint-container" style={{display: 'none'}} />
|
||||
);
|
||||
return <div id="global-hint-container" style={{ display: 'none' }} />;
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
load_config();
|
||||
listen_darkmode({default: undefined, light: false, dark: true}[window.config.color_scheme]);
|
||||
listen_darkmode(
|
||||
{ default: undefined, light: false, dark: true }[
|
||||
window.config.color_scheme
|
||||
],
|
||||
);
|
||||
this.state = {
|
||||
sidebar_stack: [[null, null]], // list of [status, content]
|
||||
mode: 'list', // list, single, search, attention
|
||||
@@ -33,13 +35,17 @@ class App extends Component {
|
||||
this.on_pressure_bound = this.on_pressure.bind(this);
|
||||
// a silly self-deceptive approach to ban guests, enough to fool those muggles
|
||||
// document cookie 'pku_ip_flag=yes'
|
||||
this.inthu_flag=window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(atob('dGh1X2lwX2ZsYWc9eWVz'))!==-1;
|
||||
this.inthu_flag =
|
||||
window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(
|
||||
atob('dGh1X2lwX2ZsYWc9eWVz'),
|
||||
) !== -1;
|
||||
}
|
||||
|
||||
static is_darkmode() {
|
||||
if (window.config.color_scheme === 'dark') return true;
|
||||
if (window.config.color_scheme === 'light') return false;
|
||||
else { // 'default'
|
||||
else {
|
||||
// 'default'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
}
|
||||
@@ -47,16 +53,14 @@ class App extends Component {
|
||||
on_pressure() {
|
||||
if (this.state.sidebar_stack.length > 1)
|
||||
this.show_sidebar(null, null, 'clear');
|
||||
else
|
||||
this.set_mode('list',null);
|
||||
else this.set_mode('list', null);
|
||||
}
|
||||
|
||||
show_sidebar(title, content, mode = 'push') {
|
||||
this.setState((prevState) => {
|
||||
let ns = prevState.sidebar_stack.slice();
|
||||
if (mode === 'push') {
|
||||
if(ns.length>MAX_SIDEBAR_STACK_SIZE)
|
||||
ns.splice(1,1);
|
||||
if (ns.length > MAX_SIDEBAR_STACK_SIZE) ns.splice(1, 1);
|
||||
ns = ns.concat([[title, content]]);
|
||||
} else if (mode === 'pop') {
|
||||
if (ns.length === 1) return;
|
||||
@@ -66,8 +70,7 @@ class App extends Component {
|
||||
ns = ns.concat([[title, content]]);
|
||||
} else if (mode === 'clear') {
|
||||
ns = [[null, null]];
|
||||
} else
|
||||
throw new Error('bad show_sidebar mode');
|
||||
} else throw new Error('bad show_sidebar mode');
|
||||
return {
|
||||
sidebar_stack: ns,
|
||||
};
|
||||
@@ -84,7 +87,8 @@ class App extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Provider value={{
|
||||
<TokenCtx.Provider
|
||||
value={{
|
||||
value: this.state.token,
|
||||
set_value: (x) => {
|
||||
localStorage['TOKEN'] = x || '';
|
||||
@@ -92,37 +96,53 @@ class App extends Component {
|
||||
token: x,
|
||||
});
|
||||
},
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<PressureHelper callback={this.on_pressure_bound} />
|
||||
<div className="bg-img" style={bgimg_style()} />
|
||||
<Title show_sidebar={this.show_sidebar_bound} set_mode={this.set_mode_bound} />
|
||||
<TokenCtx.Consumer>{(token)=>(
|
||||
<Title
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
set_mode={this.set_mode_bound}
|
||||
/>
|
||||
<TokenCtx.Consumer>
|
||||
{(token) => (
|
||||
<div className="left-container">
|
||||
<DeprecatedAlert token={token.value} />
|
||||
{!token.value &&
|
||||
{!token.value && (
|
||||
<div className="flow-item-row aux-margin">
|
||||
<div className="box box-tip">
|
||||
<p>
|
||||
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
|
||||
<LoginPopup token_callback={token.set_value}>
|
||||
{(do_popup) => (
|
||||
<a onClick={do_popup}>
|
||||
<span className="icon icon-login" />
|
||||
登录到 T大树洞
|
||||
</a>
|
||||
)}</LoginPopup>
|
||||
)}
|
||||
</LoginPopup>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{this.inthu_flag||token.value ?
|
||||
<Flow key={this.state.flow_render_key} show_sidebar={this.show_sidebar_bound}
|
||||
mode={this.state.mode} search_text={this.state.search_text} token={token.value}
|
||||
/> :
|
||||
)}
|
||||
{this.inthu_flag || token.value ? (
|
||||
<Flow
|
||||
key={this.state.flow_render_key}
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
mode={this.state.mode}
|
||||
search_text={this.state.search_text}
|
||||
token={token.value}
|
||||
/>
|
||||
) : (
|
||||
<TitleLine text="请登录后查看内容" />
|
||||
}
|
||||
)}
|
||||
<br />
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
<Sidebar show_sidebar={this.show_sidebar_bound} stack={this.state.sidebar_stack} />
|
||||
)}
|
||||
</TokenCtx.Consumer>
|
||||
<Sidebar
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
stack={this.state.sidebar_stack}
|
||||
/>
|
||||
</TokenCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,11 @@ window.audio_cache={};
|
||||
|
||||
function load_amrnb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(window.AMR)
|
||||
resolve();
|
||||
if (window.AMR) resolve();
|
||||
else
|
||||
load('static/amr_all.min.js', (err) => {
|
||||
if(err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -40,11 +37,7 @@ export class AudioWidget extends Component {
|
||||
this.setState({
|
||||
state: 'loading',
|
||||
});
|
||||
Promise.all([
|
||||
fetch(this.state.url),
|
||||
load_amrnb(),
|
||||
])
|
||||
.then((res)=>{
|
||||
Promise.all([fetch(this.state.url), load_amrnb()]).then((res) => {
|
||||
res[0].blob().then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
@@ -57,13 +50,15 @@ export class AudioWidget extends Component {
|
||||
sampleRate: 8000,
|
||||
channelCount: 1,
|
||||
bytesPerSample: 2,
|
||||
data: raw
|
||||
data: raw,
|
||||
});
|
||||
const binary_wave = new Uint8Array(wave.length);
|
||||
for (let i = 0; i < wave.length; i++)
|
||||
binary_wave[i] = wave.charCodeAt(i);
|
||||
|
||||
const objurl=URL.createObjectURL(new Blob([binary_wave], {type: 'audio/wav'}));
|
||||
const objurl = URL.createObjectURL(
|
||||
new Blob([binary_wave], { type: 'audio/wav' }),
|
||||
);
|
||||
window.audio_cache[this.state.url] = objurl;
|
||||
this.setState({
|
||||
state: 'loaded',
|
||||
@@ -80,12 +75,18 @@ export class AudioWidget extends Component {
|
||||
|
||||
render() {
|
||||
if (this.state.state === 'waiting')
|
||||
return (<p><a onClick={this.load.bind(this)}>加载音频</a></p>);
|
||||
if(this.state.state==='loading')
|
||||
return (<p>正在下载……</p>);
|
||||
else if(this.state.state==='decoding')
|
||||
return (<p>正在解码……</p>);
|
||||
return (
|
||||
<p>
|
||||
<a onClick={this.load.bind(this)}>加载音频</a>
|
||||
</p>
|
||||
);
|
||||
if (this.state.state === 'loading') return <p>正在下载……</p>;
|
||||
else if (this.state.state === 'decoding') return <p>正在解码……</p>;
|
||||
else if (this.state.state === 'loaded')
|
||||
return (<p><audio src={this.state.data} controls /></p>);
|
||||
return (
|
||||
<p>
|
||||
<audio src={this.state.data} controls />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
265
src/Common.js
265
src/Common.js
@@ -2,12 +2,18 @@ import React, {Component, PureComponent} from 'react';
|
||||
import { format_time, Time, TitleLine } from './infrastructure/widgets';
|
||||
import { THUHOLE_API_ROOT } from './flows_api';
|
||||
|
||||
import HtmlToReact from 'html-to-react'
|
||||
import HtmlToReact from 'html-to-react';
|
||||
|
||||
import './Common.css';
|
||||
import { URL_PID_RE, URL_RE, PID_RE, NICKNAME_RE, split_text } from './text_splitter';
|
||||
import {
|
||||
URL_PID_RE,
|
||||
URL_RE,
|
||||
PID_RE,
|
||||
NICKNAME_RE,
|
||||
split_text,
|
||||
} from './text_splitter';
|
||||
|
||||
import renderMd from './Markdown'
|
||||
import renderMd from './Markdown';
|
||||
|
||||
export { format_time, Time, TitleLine };
|
||||
|
||||
@@ -19,19 +25,32 @@ function escape_regex(string) {
|
||||
}
|
||||
|
||||
export function build_highlight_re(txt, split, option = 'g') {
|
||||
return txt ? new RegExp(`(${txt.split(split).filter((x)=>!!x).map(escape_regex).join('|')})`,option) : /^$/g;
|
||||
return txt
|
||||
? new RegExp(
|
||||
`(${txt
|
||||
.split(split)
|
||||
.filter((x) => !!x)
|
||||
.map(escape_regex)
|
||||
.join('|')})`,
|
||||
option,
|
||||
)
|
||||
: /^$/g;
|
||||
}
|
||||
|
||||
export function ColoredSpan(props) {
|
||||
return (
|
||||
<span className="colored-span" style={{
|
||||
<span
|
||||
className="colored-span"
|
||||
style={{
|
||||
'--coloredspan-bgcolor-light': props.colors[0],
|
||||
'--coloredspan-bgcolor-dark': props.colors[1],
|
||||
}}>{props.children}</span>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function normalize_url(url) {
|
||||
return /^https?:\/\//.test(url) ? url : 'http://' + url;
|
||||
}
|
||||
@@ -43,107 +62,172 @@ export class HighlightedText extends PureComponent {
|
||||
{this.props.parts.map((part, idx) => {
|
||||
let [rule, p] = part;
|
||||
return (
|
||||
<span key={idx}>{
|
||||
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> :
|
||||
rule==='url' ? <a href={normalize_url(p)} target="_blank" rel="noopener">{p}</a> :
|
||||
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); this.props.show_pid(p.substring(1));}}>{p}</a> :
|
||||
rule==='nickname' ? <ColoredSpan colors={this.props.color_picker.get(p)}>{p}</ColoredSpan> :
|
||||
rule==='search' ? <span className="search-query-highlight">{p}</span> :
|
||||
<span key={idx}>
|
||||
{rule === 'url_pid' ? (
|
||||
<span className="url-pid-link" title={p}>
|
||||
/##
|
||||
</span>
|
||||
) : rule === 'url' ? (
|
||||
<a href={normalize_url(p)} target="_blank" rel="noopener">
|
||||
{p}
|
||||
</a>
|
||||
) : rule === 'pid' ? (
|
||||
<a
|
||||
href={'#' + p}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.props.show_pid(p.substring(1));
|
||||
}}
|
||||
>
|
||||
{p}
|
||||
</a>
|
||||
) : rule === 'nickname' ? (
|
||||
<ColoredSpan colors={this.props.color_picker.get(p)}>
|
||||
{p}
|
||||
</ColoredSpan>
|
||||
) : rule === 'search' ? (
|
||||
<span className="search-query-highlight">{p}</span>
|
||||
) : (
|
||||
p
|
||||
}</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// props: text, show_pid, color_picker
|
||||
export class HighlightedMarkdown extends Component {
|
||||
render() {
|
||||
const props = this.props
|
||||
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React)
|
||||
const props = this.props;
|
||||
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React);
|
||||
const processInstructions = [
|
||||
{
|
||||
shouldProcessNode: (node) => node.name === 'img', // disable images
|
||||
processNode(node, children, index) {
|
||||
return (<div key={index}>[图片]</div>)
|
||||
}
|
||||
return <div key={index}>[图片]</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode: (node) => (/^h[123456]$/.test(node.name)),
|
||||
shouldProcessNode: (node) => /^h[123456]$/.test(node.name),
|
||||
processNode(node, children, index) {
|
||||
let currentLevel = +(node.name[1])
|
||||
let currentLevel = +node.name[1];
|
||||
if (currentLevel < 3) currentLevel = 3;
|
||||
const HeadingTag = `h${currentLevel}`
|
||||
return (
|
||||
<HeadingTag key={index}>{children}</HeadingTag>
|
||||
)
|
||||
}
|
||||
const HeadingTag = `h${currentLevel}`;
|
||||
return <HeadingTag key={index}>{children}</HeadingTag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode: (node) => node.name === 'a',
|
||||
processNode(node, children, index) {
|
||||
return (
|
||||
<a href={normalize_url(node.attribs.href)} target="_blank" rel="noopenner noreferrer" className="ext-link" key={index}>
|
||||
<a
|
||||
href={normalize_url(node.attribs.href)}
|
||||
target="_blank"
|
||||
rel="noopenner noreferrer"
|
||||
className="ext-link"
|
||||
key={index}
|
||||
>
|
||||
{children}
|
||||
<span className="icon icon-new-tab" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode(node) {
|
||||
return node.type === 'text' && (!node.parent || !node.parent.attribs || node.parent.attribs['encoding'] != "application/x-tex") // pid, nickname, search
|
||||
return (
|
||||
node.type === 'text' &&
|
||||
(!node.parent ||
|
||||
!node.parent.attribs ||
|
||||
node.parent.attribs['encoding'] != 'application/x-tex')
|
||||
); // pid, nickname, search
|
||||
},
|
||||
processNode(node, children, index) {
|
||||
const originalText = node.data
|
||||
const originalText = node.data;
|
||||
const splitted = split_text(originalText, [
|
||||
['url_pid', URL_PID_RE],
|
||||
['url', URL_RE],
|
||||
['pid', PID_RE],
|
||||
['nickname', NICKNAME_RE],
|
||||
])
|
||||
]);
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{splitted.map(([rule, p], idx) => {
|
||||
return (<span key={idx}>
|
||||
{
|
||||
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> :
|
||||
rule==='url' ? <a href={normalize_url(p)} className="ext-link" target="_blank" rel="noopener noreferrer">
|
||||
return (
|
||||
<span key={idx}>
|
||||
{rule === 'url_pid' ? (
|
||||
<span className="url-pid-link" title={p}>
|
||||
/##
|
||||
</span>
|
||||
) : rule === 'url' ? (
|
||||
<a
|
||||
href={normalize_url(p)}
|
||||
className="ext-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{p}
|
||||
<span className="icon icon-new-tab" />
|
||||
</a> :
|
||||
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); props.show_pid(p.substring(1));}}>{p}</a> :
|
||||
rule==='nickname' ? <ColoredSpan colors={props.color_picker.get(p)}>{p}</ColoredSpan> :
|
||||
rule==='search' ? <span className="search-query-highlight">{p}</span> :
|
||||
p}
|
||||
</span>)
|
||||
</a>
|
||||
) : rule === 'pid' ? (
|
||||
<a
|
||||
href={'#' + p}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.show_pid(p.substring(1));
|
||||
}}
|
||||
>
|
||||
{p}
|
||||
</a>
|
||||
) : rule === 'nickname' ? (
|
||||
<ColoredSpan colors={props.color_picker.get(p)}>
|
||||
{p}
|
||||
</ColoredSpan>
|
||||
) : rule === 'search' ? (
|
||||
<span className="search-query-highlight">{p}</span>
|
||||
) : (
|
||||
p
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode: () => true,
|
||||
processNode: processDefs.processDefaultNode
|
||||
}
|
||||
]
|
||||
const parser = new HtmlToReact.Parser()
|
||||
processNode: processDefs.processDefaultNode,
|
||||
},
|
||||
];
|
||||
const parser = new HtmlToReact.Parser();
|
||||
if (props.author && props.text.match(/^(?:#+ |>|```|\t|\s*-|\s*\d+\.)/)) {
|
||||
const renderedMarkdown = renderMd(props.text)
|
||||
const renderedMarkdown = renderMd(props.text);
|
||||
return (
|
||||
<>
|
||||
{props.author}
|
||||
{parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || ''}
|
||||
{parser.parseWithInstructions(
|
||||
renderedMarkdown,
|
||||
(node) => node.type !== 'script',
|
||||
processInstructions,
|
||||
) || ''}
|
||||
</>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
let rawMd = props.text
|
||||
if (props.author) rawMd = props.author + ' ' + rawMd
|
||||
const renderedMarkdown = renderMd(rawMd)
|
||||
return (parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || null)
|
||||
let rawMd = props.text;
|
||||
if (props.author) rawMd = props.author + ' ' + rawMd;
|
||||
const renderedMarkdown = renderMd(rawMd);
|
||||
return (
|
||||
parser.parseWithInstructions(
|
||||
renderedMarkdown,
|
||||
(node) => node.type !== 'script',
|
||||
processInstructions,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,11 +249,14 @@ export class SafeTextarea extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
text: window.TEXTAREA_BACKUP[this.props.id]||''
|
||||
},()=>{
|
||||
this.setState(
|
||||
{
|
||||
text: window.TEXTAREA_BACKUP[this.props.id] || '',
|
||||
},
|
||||
() => {
|
||||
this.change_callback(this.state.text);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -210,8 +297,13 @@ export class SafeTextarea extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<textarea ref={this.area_ref} onChange={this.on_change_bound} value={this.state.text} onKeyDown={this.on_keydown_bound} />
|
||||
)
|
||||
<textarea
|
||||
ref={this.area_ref}
|
||||
onChange={this.on_change_bound}
|
||||
value={this.state.text}
|
||||
onKeyDown={this.on_keydown_bound}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,28 +315,36 @@ window.addEventListener('beforeinstallprompt', (e) => {
|
||||
|
||||
export function PromotionBar(props) {
|
||||
let is_ios = /iPhone|iPad|iPod/i.test(window.navigator.userAgent);
|
||||
let is_installed=(window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone);
|
||||
let is_installed =
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
window.navigator.standalone;
|
||||
|
||||
if(is_installed)
|
||||
return null;
|
||||
if (is_installed) return null;
|
||||
|
||||
if (is_ios)
|
||||
// noinspection JSConstructorReturnsPrimitive
|
||||
return !navigator.standalone ? (
|
||||
<div className="box promotion-bar">
|
||||
<span className="icon icon-about" />
|
||||
用 Safari 把树洞 <b>添加到主屏幕</b> 更好用
|
||||
<span className="icon icon-about" />
|
||||
用 Safari 把树洞 <b>添加到主屏幕</b> 更好用
|
||||
</div>
|
||||
) : null;
|
||||
else
|
||||
// noinspection JSConstructorReturnsPrimitive
|
||||
else
|
||||
return pwa_prompt_event ? (
|
||||
<div className="box promotion-bar">
|
||||
<span className="icon icon-about" />
|
||||
把网页版树洞 <b><a onClick={()=>{
|
||||
if(pwa_prompt_event)
|
||||
pwa_prompt_event.prompt();
|
||||
}}>安装到桌面</a></b> 更好用
|
||||
<span className="icon icon-about" />
|
||||
把网页版树洞{' '}
|
||||
<b>
|
||||
<a
|
||||
onClick={() => {
|
||||
if (pwa_prompt_event) pwa_prompt_event.prompt();
|
||||
}}
|
||||
>
|
||||
安装到桌面
|
||||
</a>
|
||||
</b>{' '}
|
||||
更好用
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
@@ -275,7 +375,9 @@ export class ClickHandler extends PureComponent {
|
||||
}
|
||||
on_move(e) {
|
||||
if (!this.state.moved) {
|
||||
let mvmt=Math.abs((e.touches?e.touches[0]:e).screenY-this.state.init_y)+Math.abs((e.touches?e.touches[0]:e).screenX-this.state.init_x);
|
||||
let mvmt =
|
||||
Math.abs((e.touches ? e.touches[0] : e).screenY - this.state.init_y) +
|
||||
Math.abs((e.touches ? e.touches[0] : e).screenX - this.state.init_x);
|
||||
//console.log('move',mvmt);
|
||||
if (mvmt > this.MOVE_THRESHOLD)
|
||||
this.setState({
|
||||
@@ -285,8 +387,7 @@ export class ClickHandler extends PureComponent {
|
||||
}
|
||||
on_end(event) {
|
||||
//console.log('end');
|
||||
if(!this.state.moved)
|
||||
this.do_callback(event);
|
||||
if (!this.state.moved) this.do_callback(event);
|
||||
this.setState({
|
||||
moved: true,
|
||||
});
|
||||
@@ -300,11 +401,15 @@ export class ClickHandler extends PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div onTouchStart={this.on_begin_bound} onMouseDown={this.on_begin_bound}
|
||||
onTouchMove={this.on_move_bound} onMouseMove={this.on_move_bound}
|
||||
onClick={this.on_end_bound} >
|
||||
<div
|
||||
onTouchStart={this.on_begin_bound}
|
||||
onMouseDown={this.on_begin_bound}
|
||||
onTouchMove={this.on_move_bound}
|
||||
onMouseMove={this.on_move_bound}
|
||||
onClick={this.on_end_bound}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
172
src/Config.js
172
src/Config.js
@@ -3,22 +3,30 @@ import React, {Component, PureComponent} from 'react';
|
||||
import './Config.css';
|
||||
|
||||
const BUILTIN_IMGS = {
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg': '寻觅繁星(默认)',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/eriri.jpg': '平成著名画师',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/yurucamp.jpg': '露营天下第一',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/minecraft.jpg': '麦恩·库拉夫特',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/cyberpunk.jpg': '赛博城市',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/bj.jpg': '城市的星光',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/sif.jpg': '梦开始的地方',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg':
|
||||
'寻觅繁星(默认)',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/eriri.jpg':
|
||||
'平成著名画师',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/yurucamp.jpg':
|
||||
'露营天下第一',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/minecraft.jpg':
|
||||
'麦恩·库拉夫特',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/cyberpunk.jpg':
|
||||
'赛博城市',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/bj.jpg':
|
||||
'城市的星光',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/sif.jpg':
|
||||
'梦开始的地方',
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
background_img: 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
|
||||
background_img:
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
|
||||
background_color: '#113366',
|
||||
pressure: false,
|
||||
easter_egg: true,
|
||||
color_scheme: 'default',
|
||||
fold: true
|
||||
fold: true,
|
||||
};
|
||||
|
||||
export function load_config() {
|
||||
@@ -34,8 +42,7 @@ export function load_config() {
|
||||
|
||||
// unrecognized configs are removed
|
||||
Object.keys(loaded_config).forEach((key) => {
|
||||
if(config[key]!==undefined)
|
||||
config[key]=loaded_config[key];
|
||||
if (config[key] !== undefined) config[key] = loaded_config[key];
|
||||
});
|
||||
|
||||
console.log('config loaded', config);
|
||||
@@ -75,45 +82,71 @@ class ConfigBackground extends PureComponent {
|
||||
|
||||
on_select(e) {
|
||||
let value = e.target.value;
|
||||
this.setState({
|
||||
img: value==='##other' ? '' :
|
||||
value==='##color' ? null : value,
|
||||
},this.save_changes.bind(this));
|
||||
this.setState(
|
||||
{
|
||||
img: value === '##other' ? '' : value === '##color' ? null : value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
on_change_img(e) {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
img: e.target.value,
|
||||
},this.save_changes.bind(this));
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
on_change_color(e) {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
color: e.target.value,
|
||||
},this.save_changes.bind(this));
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let img_select= this.state.img===null ? '##color' :
|
||||
Object.keys(BUILTIN_IMGS).indexOf(this.state.img)===-1 ? '##other' : this.state.img;
|
||||
let img_select =
|
||||
this.state.img === null
|
||||
? '##color'
|
||||
: Object.keys(BUILTIN_IMGS).indexOf(this.state.img) === -1
|
||||
? '##other'
|
||||
: this.state.img;
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>背景图片:</b>
|
||||
<select value={img_select} onChange={this.on_select.bind(this)}>
|
||||
{Object.keys(BUILTIN_IMGS).map((key) => (
|
||||
<option key={key} value={key}>{BUILTIN_IMGS[key]}</option>
|
||||
<option key={key} value={key}>
|
||||
{BUILTIN_IMGS[key]}
|
||||
</option>
|
||||
))}
|
||||
<option value="##other">输入图片网址……</option>
|
||||
<option value="##color">纯色背景……</option>
|
||||
</select>
|
||||
|
||||
{img_select==='##other' &&
|
||||
<input type="url" placeholder="图片网址" value={this.state.img} onChange={this.on_change_img.bind(this)} />
|
||||
}
|
||||
{img_select==='##color' &&
|
||||
<input type="color" value={this.state.color} onChange={this.on_change_color.bind(this)} />
|
||||
}
|
||||
{img_select === '##other' && (
|
||||
<input
|
||||
type="url"
|
||||
placeholder="图片网址"
|
||||
value={this.state.img}
|
||||
onChange={this.on_change_img.bind(this)}
|
||||
/>
|
||||
)}
|
||||
{img_select === '##color' && (
|
||||
<input
|
||||
type="color"
|
||||
value={this.state.color}
|
||||
onChange={this.on_change_color.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<div className="bg-preview" style={bgimg_style(this.state.img,this.state.color)} />
|
||||
<div
|
||||
className="bg-preview"
|
||||
style={bgimg_style(this.state.img, this.state.color)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -135,9 +168,12 @@ class ConfigColorScheme extends PureComponent {
|
||||
|
||||
on_select(e) {
|
||||
let value = e.target.value;
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
color_scheme: value,
|
||||
},this.save_changes.bind(this));
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -145,18 +181,19 @@ class ConfigColorScheme extends PureComponent {
|
||||
<div>
|
||||
<p>
|
||||
<b>夜间模式:</b>
|
||||
<select value={this.state.color_scheme} onChange={this.on_select.bind(this)}>
|
||||
<select
|
||||
value={this.state.color_scheme}
|
||||
onChange={this.on_select.bind(this)}
|
||||
>
|
||||
<option value="default">跟随系统</option>
|
||||
<option value="light">始终浅色模式</option>
|
||||
<option value="dark">始终深色模式</option>
|
||||
</select>
|
||||
<small>#color_scheme</small>
|
||||
</p>
|
||||
<p>
|
||||
选择浅色或深色模式,深色模式下将会调暗图片亮度
|
||||
</p>
|
||||
<p>选择浅色或深色模式,深色模式下将会调暗图片亮度</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,13 +207,16 @@ class ConfigSwitch extends PureComponent {
|
||||
|
||||
on_change(e) {
|
||||
let val = e.target.checked;
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
switch: val,
|
||||
},()=>{
|
||||
},
|
||||
() => {
|
||||
this.props.callback({
|
||||
[this.props.id]: val,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -184,14 +224,17 @@ class ConfigSwitch extends PureComponent {
|
||||
<div>
|
||||
<p>
|
||||
<label>
|
||||
<input name={'config-'+this.props.id} type="checkbox" checked={this.state.switch} onChange={this.on_change.bind(this)} />
|
||||
<input
|
||||
name={'config-' + this.props.id}
|
||||
type="checkbox"
|
||||
checked={this.state.switch}
|
||||
onChange={this.on_change.bind(this)}
|
||||
/>
|
||||
<b>{this.props.name}</b>
|
||||
<small>#{this.props.id}</small>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
{this.props.description}
|
||||
</p>
|
||||
<p>{this.props.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -223,33 +266,62 @@ export class ConfigUI extends PureComponent {
|
||||
return (
|
||||
<div>
|
||||
<div className="box config-ui-header">
|
||||
<p>这些功能仍在测试,可能不稳定(<a onClick={this.reset_settings.bind(this)}>全部重置</a>)</p>
|
||||
<p><b>修改设置后 <a onClick={()=>{window.location.reload()}}>刷新页面</a> 方可生效</b></p>
|
||||
<p>
|
||||
这些功能仍在测试,可能不稳定(
|
||||
<a onClick={this.reset_settings.bind(this)}>全部重置</a>)
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
修改设置后{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
刷新页面
|
||||
</a>{' '}
|
||||
方可生效
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
<div className="box">
|
||||
<ConfigBackground callback={this.save_changes_bound} />
|
||||
<hr />
|
||||
<ConfigColorScheme callback={this.save_changes_bound} />
|
||||
<hr />
|
||||
<ConfigSwitch callback={this.save_changes_bound} id="pressure" name="快速返回"
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
id="pressure"
|
||||
name="快速返回"
|
||||
description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞"
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch callback={this.save_changes_bound} id="easter_egg" name="允许彩蛋"
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
id="easter_egg"
|
||||
name="允许彩蛋"
|
||||
description="在某些情况下显示彩蛋"
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch callback={this.save_changes_bound} id="fold" name="折叠树洞"
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
id="fold"
|
||||
name="折叠树洞"
|
||||
description="在时间线中折叠可能引起不适的树洞"
|
||||
/>
|
||||
<hr />
|
||||
<p>
|
||||
新功能建议或问题反馈请在
|
||||
<a href="https://github.com/thuhole/thuhole-go-backend/issues" target="_blank">GitHub <span className="icon icon-github" /></a>
|
||||
<a
|
||||
href="https://github.com/thuhole/thuhole-go-backend/issues"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub <span className="icon icon-github" />
|
||||
</a>
|
||||
提出。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
771
src/Flows.js
771
src/Flows.js
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import MarkdownItKaTeX from 'markdown-it-katex'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import './Markdown.css'
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import MarkdownItKaTeX from 'markdown-it-katex';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/atom-one-dark.css';
|
||||
import './Markdown.css';
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
let md = new MarkdownIt({
|
||||
html: false,
|
||||
@@ -14,16 +14,20 @@ let md = new MarkdownIt({
|
||||
highlight(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs"><code>' +
|
||||
return (
|
||||
'<pre class="hljs"><code>' +
|
||||
hljs.highlight(lang, str, true).value +
|
||||
'</code></pre>';
|
||||
'</code></pre>'
|
||||
);
|
||||
} catch (__) {}
|
||||
}
|
||||
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
|
||||
}
|
||||
return (
|
||||
'<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
|
||||
);
|
||||
},
|
||||
}).use(MarkdownItKaTeX, {
|
||||
"throwOnError" : false,
|
||||
"errorColor" : "#aa0000"
|
||||
})
|
||||
throwOnError: false,
|
||||
errorColor: '#aa0000',
|
||||
});
|
||||
|
||||
export default (text) => md.render(text)
|
||||
export default (text) => md.render(text);
|
||||
|
||||
@@ -17,14 +17,20 @@ export class MessageViewer extends PureComponent {
|
||||
|
||||
load() {
|
||||
if (this.state.loading_status === 'loading') return;
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
loading_status: 'loading',
|
||||
},()=>{
|
||||
fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(this.props.token)+API_VERSION_PARAM())
|
||||
},
|
||||
() => {
|
||||
fetch(
|
||||
THUHOLE_API_ROOT +
|
||||
'api_xmcp/hole/system_msg?user_token=' +
|
||||
encodeURIComponent(this.props.token) +
|
||||
API_VERSION_PARAM(),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if(json.error)
|
||||
throw new Error(json.error);
|
||||
if (json.error) throw new Error(json.error);
|
||||
else
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
@@ -37,16 +43,26 @@ export class MessageViewer extends PureComponent {
|
||||
this.setState({
|
||||
loading_status: 'failed',
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading_status === 'loading')
|
||||
return (<p className="box box-tip">加载中……</p>);
|
||||
return <p className="box box-tip">加载中……</p>;
|
||||
else if (this.state.loading_status === 'failed')
|
||||
return (<div className="box box-tip"><a onClick={()=>{this.load()}}>重新加载</a></div>);
|
||||
return (
|
||||
<div className="box box-tip">
|
||||
<a
|
||||
onClick={() => {
|
||||
this.load();
|
||||
}}
|
||||
>
|
||||
重新加载
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
else if (this.state.loading_status === 'done')
|
||||
return this.state.msg.map((msg) => (
|
||||
<div className="box">
|
||||
@@ -59,7 +75,6 @@ export class MessageViewer extends PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
else
|
||||
return null;
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Pressure from 'pressure';
|
||||
|
||||
import './PressureHelper.css';
|
||||
|
||||
const THRESHOLD=.4;
|
||||
const THRESHOLD = 0.4;
|
||||
const MULTIPLIER = 25;
|
||||
const BORDER_WIDTH = 500; // also change css!
|
||||
|
||||
@@ -38,13 +38,14 @@ export class PressureHelper extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
if (window.config.pressure) {
|
||||
Pressure.set(document.body, {
|
||||
Pressure.set(
|
||||
document.body,
|
||||
{
|
||||
change: (force) => {
|
||||
if (!this.state.fired) {
|
||||
if(force>=.999) {
|
||||
if (force >= 0.999) {
|
||||
this.do_fire();
|
||||
}
|
||||
else
|
||||
} else
|
||||
this.setState({
|
||||
level: force,
|
||||
});
|
||||
@@ -56,29 +57,32 @@ export class PressureHelper extends Component {
|
||||
fired: false,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
polyfill: false,
|
||||
only: 'touch',
|
||||
preventSelect: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!e.repeat && e.key === 'Escape') {
|
||||
if(this.esc_interval)
|
||||
clearInterval(this.esc_interval);
|
||||
this.setState({
|
||||
if (this.esc_interval) clearInterval(this.esc_interval);
|
||||
this.setState(
|
||||
{
|
||||
level: THRESHOLD / 2,
|
||||
},()=>{
|
||||
},
|
||||
() => {
|
||||
this.esc_interval = setInterval(() => {
|
||||
let new_level=this.state.level+.1;
|
||||
if(new_level>=.999)
|
||||
this.do_fire();
|
||||
let new_level = this.state.level + 0.1;
|
||||
if (new_level >= 0.999) this.do_fire();
|
||||
else
|
||||
this.setState({
|
||||
level: new_level,
|
||||
});
|
||||
}, 30);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keyup', (e) => {
|
||||
@@ -98,16 +102,19 @@ export class PressureHelper extends Component {
|
||||
render() {
|
||||
const pad = MULTIPLIER * (this.state.level - THRESHOLD) - BORDER_WIDTH;
|
||||
return (
|
||||
<div className={
|
||||
'pressure-box'
|
||||
+(this.state.fired ? ' pressure-box-fired' : '')
|
||||
+(this.state.level<=.0001 ? ' pressure-box-empty' : '')
|
||||
} style={{
|
||||
<div
|
||||
className={
|
||||
'pressure-box' +
|
||||
(this.state.fired ? ' pressure-box-fired' : '') +
|
||||
(this.state.level <= 0.0001 ? ' pressure-box-empty' : '')
|
||||
}
|
||||
style={{
|
||||
left: pad,
|
||||
right: pad,
|
||||
top: pad,
|
||||
bottom: pad,
|
||||
}} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,7 @@ export class Sidebar extends PureComponent {
|
||||
componentDidUpdate(nextProps) {
|
||||
if (this.props.stack !== nextProps.stack) {
|
||||
//console.log('sidebar top');
|
||||
if(this.sidebar_ref.current)
|
||||
this.sidebar_ref.current.scrollTop=0;
|
||||
if (this.sidebar_ref.current) this.sidebar_ref.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +24,40 @@ export class Sidebar extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
let [cur_title,cur_content]=this.props.stack[this.props.stack.length-1];
|
||||
let [cur_title, cur_content] = this.props.stack[
|
||||
this.props.stack.length - 1
|
||||
];
|
||||
return (
|
||||
<div className={'sidebar-container '+(cur_title!==null ? 'sidebar-on' : 'sidebar-off')}>
|
||||
<div className="sidebar-shadow" onClick={this.do_back_bound} onTouchEnd={(e)=>{e.preventDefault();e.target.click();}} />
|
||||
<div
|
||||
className={
|
||||
'sidebar-container ' +
|
||||
(cur_title !== null ? 'sidebar-on' : 'sidebar-off')
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="sidebar-shadow"
|
||||
onClick={this.do_back_bound}
|
||||
onTouchEnd={(e) => {
|
||||
e.preventDefault();
|
||||
e.target.click();
|
||||
}}
|
||||
/>
|
||||
<div ref={this.sidebar_ref} className="sidebar">
|
||||
{cur_content}
|
||||
</div>
|
||||
<div className="sidebar-title">
|
||||
<a className="no-underline" onClick={this.do_close_bound}> <span className="icon icon-close" /> </a>
|
||||
{this.props.stack.length>2 &&
|
||||
<a className="no-underline" onClick={this.do_back_bound}> <span className="icon icon-back" /> </a>
|
||||
}
|
||||
<a className="no-underline" onClick={this.do_close_bound}>
|
||||
|
||||
<span className="icon icon-close" />
|
||||
|
||||
</a>
|
||||
{this.props.stack.length > 2 && (
|
||||
<a className="no-underline" onClick={this.do_back_bound}>
|
||||
|
||||
<span className="icon icon-back" />
|
||||
|
||||
</a>
|
||||
)}
|
||||
{cur_title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
105
src/Title.js
105
src/Title.js
@@ -26,11 +26,14 @@ class ControlBar extends PureComponent {
|
||||
let text = decodeURIComponent(window.location.hash).substr(1);
|
||||
if (text.lastIndexOf('?') !== -1)
|
||||
text = text.substr(0, text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...'
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
search_text: text,
|
||||
}, ()=>{
|
||||
},
|
||||
() => {
|
||||
this.on_keypress({ key: 'Enter' });
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +49,20 @@ class ControlBar extends PureComponent {
|
||||
if (flag_res) {
|
||||
if (flag_res[2]) {
|
||||
localStorage[flag_res[1]] = flag_res[2];
|
||||
alert('Set Flag '+flag_res[1]+'='+flag_res[2]+'\nYou may need to refresh this webpage.');
|
||||
alert(
|
||||
'Set Flag ' +
|
||||
flag_res[1] +
|
||||
'=' +
|
||||
flag_res[2] +
|
||||
'\nYou may need to refresh this webpage.',
|
||||
);
|
||||
} else {
|
||||
delete localStorage[flag_res[1]];
|
||||
alert('Clear Flag '+flag_res[1]+'\nYou may need to refresh this webpage.');
|
||||
alert(
|
||||
'Clear Flag ' +
|
||||
flag_res[1] +
|
||||
'\nYou may need to refresh this webpage.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -77,47 +90,70 @@ class ControlBar extends PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>{({value: token})=>(
|
||||
<TokenCtx.Consumer>
|
||||
{({ value: token }) => (
|
||||
<div className="control-bar">
|
||||
<a className="no-underline control-btn" onClick={this.do_refresh_bound}>
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={this.do_refresh_bound}
|
||||
>
|
||||
<span className="icon icon-refresh" />
|
||||
<span className="control-btn-label">最新</span>
|
||||
</a>
|
||||
{!!token &&
|
||||
<a className="no-underline control-btn" onClick={this.do_attention_bound}>
|
||||
{!!token && (
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={this.do_attention_bound}
|
||||
>
|
||||
<span className="icon icon-attention" />
|
||||
<span className="control-btn-label">关注</span>
|
||||
</a>
|
||||
}
|
||||
<input className="control-search" value={this.state.search_text} placeholder="搜索 或 #树洞号"
|
||||
onChange={this.on_change_bound} onKeyPress={this.on_keypress_bound}
|
||||
)}
|
||||
<input
|
||||
className="control-search"
|
||||
value={this.state.search_text}
|
||||
placeholder="搜索 或 #树洞号"
|
||||
onChange={this.on_change_bound}
|
||||
onKeyPress={this.on_keypress_bound}
|
||||
/>
|
||||
<a className="no-underline control-btn" onClick={()=>{
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={() => {
|
||||
this.props.show_sidebar(
|
||||
'T大树洞',
|
||||
<InfoSidebar show_sidebar={this.props.show_sidebar} />
|
||||
)
|
||||
}}>
|
||||
<InfoSidebar show_sidebar={this.props.show_sidebar} />,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className={'icon icon-' + (token ? 'about' : 'login')} />
|
||||
<span className="control-btn-label">{token ? '账户' : '登录'}</span>
|
||||
<span className="control-btn-label">
|
||||
{token ? '账户' : '登录'}
|
||||
</span>
|
||||
</a>
|
||||
{!!token &&
|
||||
<a className="no-underline control-btn" onClick={()=>{
|
||||
{!!token && (
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={() => {
|
||||
this.props.show_sidebar(
|
||||
'发表树洞',
|
||||
<PostForm token={token} on_complete={()=>{
|
||||
<PostForm
|
||||
token={token}
|
||||
on_complete={() => {
|
||||
this.props.show_sidebar(null, null);
|
||||
this.do_refresh();
|
||||
}} />
|
||||
)
|
||||
}}>
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-plus" />
|
||||
<span className="control-btn-label">发表</span>
|
||||
</a>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
)
|
||||
)}
|
||||
</TokenCtx.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,16 +164,23 @@ export function Title(props) {
|
||||
<div className="aux-margin">
|
||||
<div className="title">
|
||||
<p className="centered-line">
|
||||
<span onClick={()=>props.show_sidebar(
|
||||
<span
|
||||
onClick={() =>
|
||||
props.show_sidebar(
|
||||
'T大树洞',
|
||||
<InfoSidebar show_sidebar={props.show_sidebar} />
|
||||
)}>
|
||||
<InfoSidebar show_sidebar={props.show_sidebar} />,
|
||||
)
|
||||
}
|
||||
>
|
||||
T大树洞
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} />
|
||||
<ControlBar
|
||||
show_sidebar={props.show_sidebar}
|
||||
set_mode={props.set_mode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import {API_BASE, SafeTextarea, PromotionBar, HighlightedMarkdown} from './Common';
|
||||
import {
|
||||
API_BASE,
|
||||
SafeTextarea,
|
||||
PromotionBar,
|
||||
HighlightedMarkdown,
|
||||
} from './Common';
|
||||
import { MessageViewer } from './Message';
|
||||
import { LoginPopup } from './infrastructure/widgets';
|
||||
import { ColorPicker } from './color_picker';
|
||||
@@ -7,7 +12,13 @@ import {ConfigUI} from './Config';
|
||||
import fixOrientation from 'fix-orientation';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { cache } from './cache';
|
||||
import {API_VERSION_PARAM, THUHOLE_API_ROOT, API, get_json, token_param} from './flows_api';
|
||||
import {
|
||||
API_VERSION_PARAM,
|
||||
THUHOLE_API_ROOT,
|
||||
API,
|
||||
get_json,
|
||||
token_param,
|
||||
} from './flows_api';
|
||||
|
||||
import './UserAction.css';
|
||||
|
||||
@@ -201,26 +212,35 @@ export function InfoSidebar(props) {
|
||||
<PromotionBar />
|
||||
<LoginForm show_sidebar={props.show_sidebar} />
|
||||
<div className="box list-menu">
|
||||
<a onClick={()=>{props.show_sidebar(
|
||||
'设置',
|
||||
<ConfigUI />
|
||||
)}}>
|
||||
<span className="icon icon-settings" /><label>设置</label>
|
||||
<a
|
||||
onClick={() => {
|
||||
props.show_sidebar('设置', <ConfigUI />);
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-settings" />
|
||||
<label>设置</label>
|
||||
</a>
|
||||
|
||||
<a href="https://thuhole.com/policy.html" target="_blank">
|
||||
<span className="icon icon-textfile" /><label>树洞规范(试行)</label>
|
||||
<span className="icon icon-textfile" />
|
||||
<label>树洞规范(试行)</label>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/thuhole/thuhole-go-backend/issues" target="_blank">
|
||||
<span className="icon icon-github" /><label>意见反馈</label>
|
||||
<a
|
||||
href="https://github.com/thuhole/thuhole-go-backend/issues"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="icon icon-github" />
|
||||
<label>意见反馈</label>
|
||||
</a>
|
||||
</div>
|
||||
<div className="box help-desc-box">
|
||||
<p>
|
||||
<a onClick={()=>{
|
||||
<a
|
||||
onClick={() => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
navigator.serviceWorker
|
||||
.getRegistrations()
|
||||
.then((registrations) => {
|
||||
for (let registration of registrations) {
|
||||
console.log('unregister', registration);
|
||||
@@ -232,42 +252,67 @@ export function InfoSidebar(props) {
|
||||
setTimeout(() => {
|
||||
window.location.reload(true);
|
||||
}, 200);
|
||||
}}>强制检查更新</a>
|
||||
(当前版本:【{process.env.REACT_APP_BUILD_INFO||'---'} {process.env.NODE_ENV}】 会自动在后台检查更新并在下次访问时更新)
|
||||
}}
|
||||
>
|
||||
强制检查更新
|
||||
</a>
|
||||
(当前版本:【{process.env.REACT_APP_BUILD_INFO || '---'}{' '}
|
||||
{process.env.NODE_ENV}】 会自动在后台检查更新并在下次访问时更新)
|
||||
</p>
|
||||
</div>
|
||||
<div className="box help-desc-box">
|
||||
<p>
|
||||
联系我们:thuhole at protonmail dot com
|
||||
</p>
|
||||
<p>联系我们:thuhole at protonmail dot com</p>
|
||||
</div>
|
||||
<div className="box help-desc-box">
|
||||
<p>
|
||||
T大树洞 网页版 by @thuhole,
|
||||
基于
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GPLv3</a>
|
||||
协议在 <a href="https://github.com/thuhole/webhole" target="_blank">GitHub</a> 开源
|
||||
T大树洞 网页版 by @thuhole, 基于
|
||||
<a
|
||||
href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html"
|
||||
target="_blank"
|
||||
>
|
||||
GPLv3
|
||||
</a>
|
||||
协议在{' '}
|
||||
<a href="https://github.com/thuhole/webhole" target="_blank">
|
||||
GitHub
|
||||
</a>{' '}
|
||||
开源
|
||||
</p>
|
||||
<p>
|
||||
T大树洞 网页版的诞生离不开
|
||||
<a href="https://github.com/pkuhelper-web/webhole" target="_blank" rel="noopener">P大树洞网页版 by @xmcp</a>
|
||||
<a
|
||||
href="https://github.com/pkuhelper-web/webhole"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
P大树洞网页版 by @xmcp
|
||||
</a>
|
||||
、
|
||||
<a href="https://reactjs.org/" target="_blank" rel="noopener">React</a>
|
||||
<a href="https://reactjs.org/" target="_blank" rel="noopener">
|
||||
React
|
||||
</a>
|
||||
、
|
||||
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener">IcoMoon</a>
|
||||
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener">
|
||||
IcoMoon
|
||||
</a>
|
||||
等开源项目
|
||||
</p>
|
||||
<p>
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
the Free Software Foundation, either version 3 of the License, or (at
|
||||
your option) any later version.
|
||||
</p>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GNU General Public License</a>
|
||||
<a
|
||||
href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html"
|
||||
target="_blank"
|
||||
>
|
||||
GNU General Public License
|
||||
</a>
|
||||
for more details.
|
||||
</p>
|
||||
</div>
|
||||
@@ -284,12 +329,20 @@ class ResetUsertokenWidget extends Component {
|
||||
}
|
||||
|
||||
do_reset() {
|
||||
if(window.confirm('您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!')) {
|
||||
let uid=window.prompt('您正在重置 UserToken!\n请输入您的学号以确认身份:');
|
||||
if (
|
||||
window.confirm(
|
||||
'您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!',
|
||||
)
|
||||
) {
|
||||
let uid = window.prompt(
|
||||
'您正在重置 UserToken!\n请输入您的学号以确认身份:',
|
||||
);
|
||||
if (uid)
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
loading_status: 'loading',
|
||||
},()=>{
|
||||
},
|
||||
() => {
|
||||
fetch(THUHOLE_API_ROOT + 'api_xmcp/hole/reset_usertoken', {
|
||||
method: 'post',
|
||||
headers: {
|
||||
@@ -302,10 +355,8 @@ class ResetUsertokenWidget extends Component {
|
||||
})
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if(json.error)
|
||||
throw new Error(json.error);
|
||||
else
|
||||
alert('重置成功!您需要在所有设备上重新登录。');
|
||||
if (json.error) throw new Error(json.error);
|
||||
else alert('重置成功!您需要在所有设备上重新登录。');
|
||||
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
@@ -316,38 +367,48 @@ class ResetUsertokenWidget extends Component {
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading_status === 'done')
|
||||
return (<a onClick={this.do_reset.bind(this)}>重置</a>);
|
||||
return <a onClick={this.do_reset.bind(this)}>重置</a>;
|
||||
else if (this.state.loading_status === 'loading')
|
||||
return (<a><span className="icon icon-loading" /></a>);
|
||||
return (
|
||||
<a>
|
||||
<span className="icon icon-loading" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class LoginForm extends Component {
|
||||
copy_token(token) {
|
||||
if(copy(token))
|
||||
alert('复制成功!\n请一定不要泄露哦');
|
||||
if (copy(token)) alert('复制成功!\n请一定不要泄露哦');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>{(token)=>
|
||||
<TokenCtx.Consumer>
|
||||
{(token) => (
|
||||
<div>
|
||||
{/*{!!token.value &&*/}
|
||||
{/* <LifeInfoBox token={token.value} set_token={token.set_value} />*/}
|
||||
{/*}*/}
|
||||
<div className="login-form box">
|
||||
{token.value ?
|
||||
{token.value ? (
|
||||
<div>
|
||||
<p>
|
||||
<b>您已登录。</b>
|
||||
<button type="button" onClick={()=>{token.set_value(null);}}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
token.set_value(null);
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-logout" /> 注销
|
||||
</button>
|
||||
<br />
|
||||
@@ -357,18 +418,32 @@ export class LoginForm extends Component {
|
||||
{/*T大树洞将会单向加密(i.e. 哈希散列)您的邮箱后再存入数据库,因此您的发帖具有较强的匿名性。具体可见我们的<a href="https://github.com/thuhole/thuhole-go-backend/blob/76f56e6b75257b59e552b6bdba77e114151fcad1/src/db.go#L184">后端开源代码</a>。*/}
|
||||
{/*</p>*/}
|
||||
<p>
|
||||
<a onClick={()=>{this.props.show_sidebar(
|
||||
<a
|
||||
onClick={() => {
|
||||
this.props.show_sidebar(
|
||||
'系统消息',
|
||||
<MessageViewer token={token.value} />
|
||||
)}}>查看系统消息</a><br />
|
||||
<MessageViewer token={token.value} />,
|
||||
);
|
||||
}}
|
||||
>
|
||||
查看系统消息
|
||||
</a>
|
||||
<br />
|
||||
当您发送的内容违规时,我们将用系统消息提示您
|
||||
</p>
|
||||
<p>
|
||||
<a onClick={this.copy_token.bind(this,token.value)}>复制 User Token</a><br />
|
||||
复制 User Token 可以在新设备登录,切勿告知他人。若怀疑被盗号请重新邮箱验证码登录以重置Token。{/*,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />*/}
|
||||
<a onClick={this.copy_token.bind(this, token.value)}>
|
||||
复制 User Token
|
||||
</a>
|
||||
<br />
|
||||
复制 User Token
|
||||
可以在新设备登录,切勿告知他人。若怀疑被盗号请重新邮箱验证码登录以重置Token。
|
||||
{/*,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />*/}
|
||||
</p>
|
||||
</div> :
|
||||
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
|
||||
</div>
|
||||
) : (
|
||||
<LoginPopup token_callback={token.set_value}>
|
||||
{(do_popup) => (
|
||||
<div>
|
||||
<p>
|
||||
<button type="button" onClick={do_popup}>
|
||||
@@ -376,16 +451,21 @@ export class LoginForm extends Component {
|
||||
登录
|
||||
</button>
|
||||
</p>
|
||||
<p><small>
|
||||
T大树洞 面向T大学生,通过T大邮箱验证您的身份并提供服务。
|
||||
</small></p>
|
||||
<p>
|
||||
<small>
|
||||
T大树洞
|
||||
面向T大学生,通过T大邮箱验证您的身份并提供服务。
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
)}</LoginPopup>
|
||||
}
|
||||
)}
|
||||
</LoginPopup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}</TokenCtx.Consumer>
|
||||
)
|
||||
)}
|
||||
</TokenCtx.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,12 +479,19 @@ export class ReplyForm extends Component {
|
||||
};
|
||||
this.on_change_bound = this.on_change.bind(this);
|
||||
this.area_ref = this.props.area_ref || React.createRef();
|
||||
this.global_keypress_handler_bound=this.global_keypress_handler.bind(this);
|
||||
this.global_keypress_handler_bound = this.global_keypress_handler.bind(
|
||||
this,
|
||||
);
|
||||
this.color_picker = new ColorPicker();
|
||||
}
|
||||
|
||||
global_keypress_handler(e) {
|
||||
if(e.code==='Enter' && !e.ctrlKey && !e.altKey && ['input','textarea'].indexOf(e.target.tagName.toLowerCase())===-1) {
|
||||
if (
|
||||
e.code === 'Enter' &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) === -1
|
||||
) {
|
||||
if (this.area_ref.current) {
|
||||
e.preventDefault();
|
||||
this.area_ref.current.focus();
|
||||
@@ -415,7 +502,10 @@ export class ReplyForm extends Component {
|
||||
document.addEventListener('keypress', this.global_keypress_handler_bound);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keypress',this.global_keypress_handler_bound);
|
||||
document.removeEventListener(
|
||||
'keypress',
|
||||
this.global_keypress_handler_bound,
|
||||
);
|
||||
}
|
||||
|
||||
on_change(value) {
|
||||
@@ -426,8 +516,7 @@ export class ReplyForm extends Component {
|
||||
|
||||
on_submit(event) {
|
||||
if (event) event.preventDefault();
|
||||
if(this.state.loading_status==='loading')
|
||||
return;
|
||||
if (this.state.loading_status === 'loading') return;
|
||||
this.setState({
|
||||
loading_status: 'loading',
|
||||
});
|
||||
@@ -436,13 +525,16 @@ export class ReplyForm extends Component {
|
||||
data.append('pid', this.props.pid);
|
||||
data.append('text', this.state.text);
|
||||
data.append('user_token', this.props.token);
|
||||
fetch(API_BASE+'/api.php?action=docomment'+token_param(this.props.token), {
|
||||
fetch(
|
||||
API_BASE + '/api.php?action=docomment' + token_param(this.props.token),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
},
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
@@ -469,33 +561,55 @@ export class ReplyForm extends Component {
|
||||
|
||||
toggle_preview() {
|
||||
this.setState({
|
||||
preview: !this.state.preview
|
||||
preview: !this.state.preview,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.on_submit.bind(this)} className={'reply-form box'+(this.state.text?' reply-sticky':'')}>
|
||||
{
|
||||
this.state.preview ?
|
||||
<div className='reply-preview'>
|
||||
<HighlightedMarkdown text={this.state.text} color_picker={this.color_picker} show_pid={()=>{}} />
|
||||
</div> :
|
||||
<SafeTextarea ref={this.area_ref} id={this.props.pid} on_change={this.on_change_bound} on_submit={this.on_submit.bind(this)} />
|
||||
}
|
||||
<button type='button' onClick={()=>{this.toggle_preview()}}>
|
||||
{this.state.preview? <span className="icon icon-eye-blocked" />: <span className="icon icon-eye" />}
|
||||
<form
|
||||
onSubmit={this.on_submit.bind(this)}
|
||||
className={'reply-form box' + (this.state.text ? ' reply-sticky' : '')}
|
||||
>
|
||||
{this.state.preview ? (
|
||||
<div className="reply-preview">
|
||||
<HighlightedMarkdown
|
||||
text={this.state.text}
|
||||
color_picker={this.color_picker}
|
||||
show_pid={() => {}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SafeTextarea
|
||||
ref={this.area_ref}
|
||||
id={this.props.pid}
|
||||
on_change={this.on_change_bound}
|
||||
on_submit={this.on_submit.bind(this)}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
this.toggle_preview();
|
||||
}}
|
||||
>
|
||||
{this.state.preview ? (
|
||||
<span className="icon icon-eye-blocked" />
|
||||
) : (
|
||||
<span className="icon icon-eye" />
|
||||
)}
|
||||
</button>
|
||||
{this.state.loading_status==='loading' ?
|
||||
{this.state.loading_status === 'loading' ? (
|
||||
<button disabled="disabled">
|
||||
<span className="icon icon-loading" />
|
||||
</button> :
|
||||
</button>
|
||||
) : (
|
||||
<button type="submit">
|
||||
<span className="icon icon-send" />
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,8 +630,7 @@ export class PostForm extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if(this.area_ref.current)
|
||||
this.area_ref.current.focus();
|
||||
if (this.area_ref.current) this.area_ref.current.focus();
|
||||
}
|
||||
|
||||
on_change(value) {
|
||||
@@ -531,8 +644,7 @@ export class PostForm extends Component {
|
||||
data.append('text', this.state.text);
|
||||
data.append('type', img ? 'image' : 'text');
|
||||
data.append('user_token', this.props.token);
|
||||
if(img)
|
||||
data.append('data',img);
|
||||
if (img) data.append('data', img);
|
||||
|
||||
fetch(API_BASE + '/api.php?action=dopost' + token_param(this.props.token), {
|
||||
method: 'POST',
|
||||
@@ -569,8 +681,7 @@ export class PostForm extends Component {
|
||||
return new Promise((resolve, reject) => {
|
||||
function return_url(url) {
|
||||
const idx = url.indexOf(';base64,');
|
||||
if(idx===-1)
|
||||
throw new Error('img not base64 encoded');
|
||||
if (idx === -1) throw new Error('img not base64 encoded');
|
||||
|
||||
return url.substr(idx + 8);
|
||||
}
|
||||
@@ -578,23 +689,23 @@ export class PostForm extends Component {
|
||||
let reader = new FileReader();
|
||||
function on_got_img(url) {
|
||||
const image = new Image();
|
||||
image.onload=(()=>{
|
||||
image.onload = () => {
|
||||
let width = image.width;
|
||||
let height = image.height;
|
||||
let compressed = false;
|
||||
|
||||
if (width > MAX_IMG_DIAM) {
|
||||
height=height*MAX_IMG_DIAM/width;
|
||||
height = (height * MAX_IMG_DIAM) / width;
|
||||
width = MAX_IMG_DIAM;
|
||||
compressed = true;
|
||||
}
|
||||
if (height > MAX_IMG_DIAM) {
|
||||
width=width*MAX_IMG_DIAM/height;
|
||||
width = (width * MAX_IMG_DIAM) / height;
|
||||
height = MAX_IMG_DIAM;
|
||||
compressed = true;
|
||||
}
|
||||
if (height * width > MAX_IMG_PX) {
|
||||
let rate=Math.sqrt(height*width/MAX_IMG_PX);
|
||||
let rate = Math.sqrt((height * width) / MAX_IMG_PX);
|
||||
height /= rate;
|
||||
width /= rate;
|
||||
compressed = true;
|
||||
@@ -607,17 +718,25 @@ export class PostForm extends Component {
|
||||
canvas.height = height;
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
|
||||
let quality_l=.1,quality_r=.9,quality,new_url;
|
||||
while(quality_r-quality_l>=.03) {
|
||||
let quality_l = 0.1,
|
||||
quality_r = 0.9,
|
||||
quality,
|
||||
new_url;
|
||||
while (quality_r - quality_l >= 0.03) {
|
||||
quality = (quality_r + quality_l) / 2;
|
||||
new_url = canvas.toDataURL('image/jpeg', quality);
|
||||
console.log(quality_l,quality_r,'trying quality',quality,'size',new_url.length);
|
||||
if(new_url.length<=MAX_IMG_FILESIZE)
|
||||
quality_l=quality;
|
||||
else
|
||||
quality_r=quality;
|
||||
console.log(
|
||||
quality_l,
|
||||
quality_r,
|
||||
'trying quality',
|
||||
quality,
|
||||
'size',
|
||||
new_url.length,
|
||||
);
|
||||
if (new_url.length <= MAX_IMG_FILESIZE) quality_l = quality;
|
||||
else quality_r = quality;
|
||||
}
|
||||
if(quality_l>=.101) {
|
||||
if (quality_l >= 0.101) {
|
||||
console.log('chosen img quality', quality);
|
||||
resolve({
|
||||
img: return_url(new_url),
|
||||
@@ -629,7 +748,7 @@ export class PostForm extends Component {
|
||||
} else {
|
||||
reject('图片过大,无法上传');
|
||||
}
|
||||
});
|
||||
};
|
||||
image.src = url;
|
||||
}
|
||||
reader.onload = (event) => {
|
||||
@@ -643,14 +762,21 @@ export class PostForm extends Component {
|
||||
|
||||
on_img_change() {
|
||||
if (this.img_ref.current && this.img_ref.current.files.length)
|
||||
this.setState({
|
||||
img_tip: '(正在处理图片……)'
|
||||
},()=>{
|
||||
this.setState(
|
||||
{
|
||||
img_tip: '(正在处理图片……)',
|
||||
},
|
||||
() => {
|
||||
this.proc_img(this.img_ref.current.files[0])
|
||||
.then((d) => {
|
||||
this.setState({
|
||||
img_tip: `(${d.compressed?'压缩到':'尺寸'} ${d.width}*${d.height} / `+
|
||||
`质量 ${Math.floor(d.quality*100)}% / ${Math.floor(d.img.length/BASE64_RATE/1000)}KB)`,
|
||||
img_tip:
|
||||
`(${d.compressed ? '压缩到' : '尺寸'} ${d.width}*${
|
||||
d.height
|
||||
} / ` +
|
||||
`质量 ${Math.floor(d.quality * 100)}% / ${Math.floor(
|
||||
d.img.length / BASE64_RATE / 1000,
|
||||
)}KB)`,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -658,7 +784,8 @@ export class PostForm extends Component {
|
||||
img_tip: `图片无效:${e}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
else
|
||||
this.setState({
|
||||
img_tip: null,
|
||||
@@ -667,8 +794,7 @@ export class PostForm extends Component {
|
||||
|
||||
on_submit(event) {
|
||||
if (event) event.preventDefault();
|
||||
if(this.state.loading_status==='loading')
|
||||
return;
|
||||
if (this.state.loading_status === 'loading') return;
|
||||
if (this.img_ref.current.files.length) {
|
||||
this.setState({
|
||||
loading_status: 'processing',
|
||||
@@ -693,7 +819,7 @@ export class PostForm extends Component {
|
||||
|
||||
toggle_preview() {
|
||||
this.setState({
|
||||
preview: !this.state.preview
|
||||
preview: !this.state.preview,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -703,52 +829,89 @@ export class PostForm extends Component {
|
||||
<div className="post-form-bar">
|
||||
<label>
|
||||
图片
|
||||
<input ref={this.img_ref} type="file" accept="image/*" disabled={this.state.loading_status!=='done'}
|
||||
<input
|
||||
ref={this.img_ref}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
disabled={this.state.loading_status !== 'done'}
|
||||
onChange={this.on_img_change_bound}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{
|
||||
this.state.preview ?
|
||||
<button type='button' onClick={()=>{this.toggle_preview()}}>
|
||||
{this.state.preview ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
this.toggle_preview();
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-eye-blocked" />
|
||||
编辑
|
||||
</button> :
|
||||
<button type='button' onClick={()=>{this.toggle_preview()}}>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
this.toggle_preview();
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-eye" />
|
||||
预览
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
|
||||
{
|
||||
this.state.loading_status!=='done' ?
|
||||
{this.state.loading_status !== 'done' ? (
|
||||
<button disabled="disabled">
|
||||
<span className="icon icon-loading" />
|
||||
{this.state.loading_status==='processing' ? '处理' : '上传'}
|
||||
</button> :
|
||||
|
||||
{this.state.loading_status === 'processing' ? '处理' : '上传'}
|
||||
</button>
|
||||
) : (
|
||||
<button type="submit">
|
||||
<span className="icon icon-send" />
|
||||
发表
|
||||
</button>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{!!this.state.img_tip &&
|
||||
{!!this.state.img_tip && (
|
||||
<p className="post-form-img-tip">
|
||||
<a onClick={()=>{this.img_ref.current.value=""; this.on_img_change();}}>删除图片</a>
|
||||
<a
|
||||
onClick={() => {
|
||||
this.img_ref.current.value = '';
|
||||
this.on_img_change();
|
||||
}}
|
||||
>
|
||||
删除图片
|
||||
</a>
|
||||
{this.state.img_tip}
|
||||
</p>
|
||||
}
|
||||
{
|
||||
this.state.preview ?
|
||||
<div className='post-preview'>
|
||||
<HighlightedMarkdown text={this.state.text} color_picker={this.color_picker} show_pid={()=>{}} />
|
||||
</div> :
|
||||
<SafeTextarea ref={this.area_ref} id="new_post" on_change={this.on_change_bound} on_submit={this.on_submit.bind(this)} />
|
||||
}
|
||||
<p><small>
|
||||
请遵守<a href="https://thuhole.com/policy.html" target="_blank">树洞管理规范(试行)</a>,文明发言
|
||||
</small></p>
|
||||
)}
|
||||
{this.state.preview ? (
|
||||
<div className="post-preview">
|
||||
<HighlightedMarkdown
|
||||
text={this.state.text}
|
||||
color_picker={this.color_picker}
|
||||
show_pid={() => {}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SafeTextarea
|
||||
ref={this.area_ref}
|
||||
id="new_post"
|
||||
on_change={this.on_change_bound}
|
||||
on_submit={this.on_submit.bind(this)}
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
<small>
|
||||
请遵守
|
||||
<a href="https://thuhole.com/policy.html" target="_blank">
|
||||
树洞管理规范(试行)
|
||||
</a>
|
||||
,文明发言
|
||||
</small>
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
43
src/cache.js
43
src/cache.js
@@ -44,8 +44,7 @@ class Cache {
|
||||
// use window.hole_cache.decrypt() only after cache is loaded!
|
||||
decrypt(pid, s) {
|
||||
let o = '';
|
||||
if(typeof(s)!==typeof('str'))
|
||||
return null;
|
||||
if (typeof s !== typeof 'str') return null;
|
||||
|
||||
for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) {
|
||||
let c = key ^ s.charCodeAt(i);
|
||||
@@ -65,8 +64,7 @@ class Cache {
|
||||
get(pid, target_version) {
|
||||
pid = parseInt(pid);
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!this.db)
|
||||
return resolve(null);
|
||||
if (!this.db) return resolve(null);
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
const get_req = store.get(pid);
|
||||
@@ -75,14 +73,23 @@ class Cache {
|
||||
if (!res || !res.data_str) {
|
||||
//console.log('comment cache miss '+pid);
|
||||
resolve(null);
|
||||
} else if(target_version===res.version) { // hit
|
||||
} else if (target_version === res.version) {
|
||||
// hit
|
||||
console.log('comment cache hit', pid);
|
||||
res.last_access=(+new Date());
|
||||
res.last_access = +new Date();
|
||||
store.put(res);
|
||||
let data = this.decrypt(pid, res.data_str);
|
||||
resolve(data); // obj or null
|
||||
} else { // expired
|
||||
console.log('comment cache expired',pid,': ver',res.version,'target',target_version);
|
||||
} else {
|
||||
// expired
|
||||
console.log(
|
||||
'comment cache expired',
|
||||
pid,
|
||||
': ver',
|
||||
res.version,
|
||||
'target',
|
||||
target_version,
|
||||
);
|
||||
store.delete(pid);
|
||||
resolve(null);
|
||||
}
|
||||
@@ -98,8 +105,7 @@ class Cache {
|
||||
put(pid, target_version, data) {
|
||||
pid = parseInt(pid);
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!this.db)
|
||||
return resolve();
|
||||
if (!this.db) return resolve();
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
store.put({
|
||||
@@ -116,8 +122,7 @@ class Cache {
|
||||
delete(pid) {
|
||||
pid = parseInt(pid);
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!this.db)
|
||||
return resolve();
|
||||
if (!this.db) return resolve();
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
let req = store.delete(pid);
|
||||
@@ -131,8 +136,7 @@ class Cache {
|
||||
}
|
||||
|
||||
maintenance() {
|
||||
if(!this.db)
|
||||
return;
|
||||
if (!this.db) return;
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
let count_req = store.count();
|
||||
@@ -145,8 +149,7 @@ class Cache {
|
||||
if (cur) {
|
||||
//console.log('maintenance: delete',cur);
|
||||
store.delete(cur.primaryKey);
|
||||
if(--count>MAINTENANCE_COUNT)
|
||||
cur.continue();
|
||||
if (--count > MAINTENANCE_COUNT) cur.continue();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@@ -158,15 +161,13 @@ class Cache {
|
||||
}
|
||||
|
||||
clear() {
|
||||
if(!this.db)
|
||||
return;
|
||||
if (!this.db) return;
|
||||
indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME);
|
||||
console.log('delete comment cache db');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function cache() {
|
||||
if(!window.hole_cache)
|
||||
window.hole_cache=new Cache();
|
||||
if (!window.hole_cache) window.hole_cache = new Cache();
|
||||
return window.hole_cache;
|
||||
}
|
||||
@@ -10,8 +10,7 @@ export class ColorPicker {
|
||||
|
||||
get(name) {
|
||||
name = name.toLowerCase();
|
||||
if(name==='洞主')
|
||||
return ['hsl(0,0%,97%)','hsl(0,0%,16%)'];
|
||||
if (name === '洞主') return ['hsl(0,0%,97%)', 'hsl(0,0%,16%)'];
|
||||
|
||||
if (!this.names[name]) {
|
||||
this.current_h += golden_ratio_conjugate;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {cache} from './cache';
|
||||
export { THUHOLE_API_ROOT, API_VERSION_PARAM };
|
||||
|
||||
export function token_param(token) {
|
||||
return API_VERSION_PARAM()+(token ? ('&user_token='+token) : '');
|
||||
return API_VERSION_PARAM() + (token ? '&user_token=' + token : '');
|
||||
}
|
||||
|
||||
export { get_json };
|
||||
@@ -17,9 +17,11 @@ export const API={
|
||||
load_replies: (pid, token, color_picker, cache_version) => {
|
||||
pid = parseInt(pid);
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getcomment'+
|
||||
'&pid='+pid+
|
||||
token_param(token)
|
||||
API_BASE +
|
||||
'/api.php?action=getcomment' +
|
||||
'&pid=' +
|
||||
pid +
|
||||
token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
@@ -28,7 +30,9 @@ export const API={
|
||||
else throw new Error(JSON.stringify(json));
|
||||
}
|
||||
|
||||
cache().delete(pid).then(()=>{
|
||||
cache()
|
||||
.delete(pid)
|
||||
.then(() => {
|
||||
cache().put(pid, cache_version, json);
|
||||
});
|
||||
|
||||
@@ -49,7 +53,8 @@ export const API={
|
||||
|
||||
load_replies_with_cache: (pid, token, color_picker, cache_version) => {
|
||||
pid = parseInt(pid);
|
||||
return cache().get(pid,cache_version)
|
||||
return cache()
|
||||
.get(pid, cache_version)
|
||||
.then((json) => {
|
||||
if (json) {
|
||||
// also change load_replies!
|
||||
@@ -64,9 +69,7 @@ export const API={
|
||||
});
|
||||
|
||||
return json;
|
||||
}
|
||||
else
|
||||
return API.load_replies(pid,token,color_picker,cache_version);
|
||||
} else return API.load_replies(pid, token, color_picker, cache_version);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -86,8 +89,8 @@ export const API={
|
||||
.then((json) => {
|
||||
cache().delete(pid);
|
||||
if (json.code !== 0) {
|
||||
if(json.msg && json.msg==='已经关注过了') {}
|
||||
else {
|
||||
if (json.msg && json.msg === '已经关注过了') {
|
||||
} else {
|
||||
if (json.msg) alert(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
@@ -120,25 +123,26 @@ export const API={
|
||||
|
||||
get_list: (page, token) => {
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getlist'+
|
||||
'&p='+page+
|
||||
token_param(token)
|
||||
API_BASE + '/api.php?action=getlist' + '&p=' + page + token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if(json.code!==0)
|
||||
throw new Error(JSON.stringify(json));
|
||||
if (json.code !== 0) throw new Error(JSON.stringify(json));
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
get_search: (page, keyword, token) => {
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=search'+
|
||||
'&pagesize='+SEARCH_PAGESIZE+
|
||||
'&page='+page+
|
||||
'&keywords='+encodeURIComponent(keyword)+
|
||||
token_param(token)
|
||||
API_BASE +
|
||||
'/api.php?action=search' +
|
||||
'&pagesize=' +
|
||||
SEARCH_PAGESIZE +
|
||||
'&page=' +
|
||||
page +
|
||||
'&keywords=' +
|
||||
encodeURIComponent(keyword) +
|
||||
token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
@@ -152,9 +156,7 @@ export const API={
|
||||
|
||||
get_single: (pid, token) => {
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getone'+
|
||||
'&pid='+pid+
|
||||
token_param(token)
|
||||
API_BASE + '/api.php?action=getone' + '&pid=' + pid + token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
@@ -167,10 +169,7 @@ export const API={
|
||||
},
|
||||
|
||||
get_attention: (token) => {
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getattention'+
|
||||
token_param(token)
|
||||
)
|
||||
return fetch(API_BASE + '/api.php?action=getattention' + token_param(token))
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
|
||||
@@ -14,8 +14,8 @@ const isLocalhost = Boolean(
|
||||
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}$/
|
||||
)
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
@@ -41,7 +41,7 @@ export default function register() {
|
||||
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'
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -55,7 +55,7 @@ export default function register() {
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
@@ -76,7 +76,7 @@ function registerValidSW(swUrl) {
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
@@ -84,14 +84,14 @@ function registerValidSW(swUrl) {
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
.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 => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
@@ -103,14 +103,14 @@ function checkValidServiceWorker(swUrl) {
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
'No internet connection found. App is running in offline mode.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,21 +14,21 @@ export function split_text(txt,rules) {
|
||||
txt = [[null, txt]];
|
||||
rules.forEach((rule) => {
|
||||
let [name, regex] = rule;
|
||||
txt=[].concat.apply([],txt.map((part)=>{
|
||||
txt = [].concat.apply(
|
||||
[],
|
||||
txt.map((part) => {
|
||||
let [rule, content] = part;
|
||||
if(rule) // already tagged by previous rules
|
||||
if (rule)
|
||||
// already tagged by previous rules
|
||||
return [part];
|
||||
else {
|
||||
return content
|
||||
.split(regex)
|
||||
.map((seg)=>(
|
||||
regex.test(seg) ? [name,seg] : [null,seg]
|
||||
))
|
||||
.filter(([name,seg])=>(
|
||||
name!==null || seg
|
||||
));
|
||||
.map((seg) => (regex.test(seg) ? [name, seg] : [null, seg]))
|
||||
.filter(([name, seg]) => name !== null || seg);
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
});
|
||||
return txt;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user