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