format code
This commit is contained in:
252
src/App.js
252
src/App.js
@@ -1,131 +1,151 @@
|
||||
import React, {Component} from 'react';
|
||||
import {Flow} from './Flows';
|
||||
import {Title} from './Title';
|
||||
import {Sidebar} from './Sidebar';
|
||||
import {PressureHelper} from './PressureHelper';
|
||||
import {TokenCtx} from './UserAction';
|
||||
import {load_config,bgimg_style} from './Config';
|
||||
import {listen_darkmode} from './infrastructure/functions';
|
||||
import {LoginPopup, TitleLine} from './infrastructure/widgets';
|
||||
import React, { Component } from 'react';
|
||||
import { Flow } from './Flows';
|
||||
import { Title } from './Title';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { PressureHelper } from './PressureHelper';
|
||||
import { TokenCtx } from './UserAction';
|
||||
import { load_config, bgimg_style } from './Config';
|
||||
import { listen_darkmode } from './infrastructure/functions';
|
||||
import { LoginPopup, TitleLine } from './infrastructure/widgets';
|
||||
|
||||
const MAX_SIDEBAR_STACK_SIZE=10;
|
||||
const MAX_SIDEBAR_STACK_SIZE = 10;
|
||||
|
||||
function DeprecatedAlert(props) {
|
||||
return (
|
||||
<div id="global-hint-container" style={{display: 'none'}} />
|
||||
);
|
||||
return <div id="global-hint-container" style={{ display: 'none' }} />;
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
load_config();
|
||||
listen_darkmode({default: undefined, light: false, dark: true}[window.config.color_scheme]);
|
||||
this.state={
|
||||
sidebar_stack: [[null,null]], // list of [status, content]
|
||||
mode: 'list', // list, single, search, attention
|
||||
search_text: null,
|
||||
flow_render_key: +new Date(),
|
||||
token: localStorage['TOKEN']||null,
|
||||
};
|
||||
this.show_sidebar_bound=this.show_sidebar.bind(this);
|
||||
this.set_mode_bound=this.set_mode.bind(this);
|
||||
this.on_pressure_bound=this.on_pressure.bind(this);
|
||||
// a silly self-deceptive approach to ban guests, enough to fool those muggles
|
||||
// document cookie 'pku_ip_flag=yes'
|
||||
this.inthu_flag=window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(atob('dGh1X2lwX2ZsYWc9eWVz'))!==-1;
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
load_config();
|
||||
listen_darkmode(
|
||||
{ default: undefined, light: false, dark: true }[
|
||||
window.config.color_scheme
|
||||
],
|
||||
);
|
||||
this.state = {
|
||||
sidebar_stack: [[null, null]], // list of [status, content]
|
||||
mode: 'list', // list, single, search, attention
|
||||
search_text: null,
|
||||
flow_render_key: +new Date(),
|
||||
token: localStorage['TOKEN'] || null,
|
||||
};
|
||||
this.show_sidebar_bound = this.show_sidebar.bind(this);
|
||||
this.set_mode_bound = this.set_mode.bind(this);
|
||||
this.on_pressure_bound = this.on_pressure.bind(this);
|
||||
// a silly self-deceptive approach to ban guests, enough to fool those muggles
|
||||
// document cookie 'pku_ip_flag=yes'
|
||||
this.inthu_flag =
|
||||
window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(
|
||||
atob('dGh1X2lwX2ZsYWc9eWVz'),
|
||||
) !== -1;
|
||||
}
|
||||
|
||||
static is_darkmode() {
|
||||
if(window.config.color_scheme==='dark') return true;
|
||||
if(window.config.color_scheme==='light') return false;
|
||||
else { // 'default'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
static is_darkmode() {
|
||||
if (window.config.color_scheme === 'dark') return true;
|
||||
if (window.config.color_scheme === 'light') return false;
|
||||
else {
|
||||
// 'default'
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
}
|
||||
|
||||
on_pressure() {
|
||||
if(this.state.sidebar_stack.length>1)
|
||||
this.show_sidebar(null,null,'clear');
|
||||
else
|
||||
this.set_mode('list',null);
|
||||
}
|
||||
on_pressure() {
|
||||
if (this.state.sidebar_stack.length > 1)
|
||||
this.show_sidebar(null, null, 'clear');
|
||||
else this.set_mode('list', null);
|
||||
}
|
||||
|
||||
show_sidebar(title,content,mode='push') {
|
||||
this.setState((prevState)=>{
|
||||
let ns=prevState.sidebar_stack.slice();
|
||||
if(mode==='push') {
|
||||
if(ns.length>MAX_SIDEBAR_STACK_SIZE)
|
||||
ns.splice(1,1);
|
||||
ns=ns.concat([[title,content]]);
|
||||
} else if(mode==='pop') {
|
||||
if(ns.length===1) return;
|
||||
ns.pop();
|
||||
} else if(mode==='replace') {
|
||||
ns.pop();
|
||||
ns=ns.concat([[title,content]]);
|
||||
} else if(mode==='clear') {
|
||||
ns=[[null,null]];
|
||||
} else
|
||||
throw new Error('bad show_sidebar mode');
|
||||
return {
|
||||
sidebar_stack: ns,
|
||||
};
|
||||
});
|
||||
}
|
||||
show_sidebar(title, content, mode = 'push') {
|
||||
this.setState((prevState) => {
|
||||
let ns = prevState.sidebar_stack.slice();
|
||||
if (mode === 'push') {
|
||||
if (ns.length > MAX_SIDEBAR_STACK_SIZE) ns.splice(1, 1);
|
||||
ns = ns.concat([[title, content]]);
|
||||
} else if (mode === 'pop') {
|
||||
if (ns.length === 1) return;
|
||||
ns.pop();
|
||||
} else if (mode === 'replace') {
|
||||
ns.pop();
|
||||
ns = ns.concat([[title, content]]);
|
||||
} else if (mode === 'clear') {
|
||||
ns = [[null, null]];
|
||||
} else throw new Error('bad show_sidebar mode');
|
||||
return {
|
||||
sidebar_stack: ns,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
set_mode(mode,search_text) {
|
||||
this.setState({
|
||||
mode: mode,
|
||||
search_text: search_text,
|
||||
flow_render_key: +new Date(),
|
||||
});
|
||||
}
|
||||
set_mode(mode, search_text) {
|
||||
this.setState({
|
||||
mode: mode,
|
||||
search_text: search_text,
|
||||
flow_render_key: +new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Provider value={{
|
||||
value: this.state.token,
|
||||
set_value: (x)=>{
|
||||
localStorage['TOKEN']=x||'';
|
||||
this.setState({
|
||||
token: x,
|
||||
});
|
||||
},
|
||||
}}>
|
||||
<PressureHelper callback={this.on_pressure_bound} />
|
||||
<div className="bg-img" style={bgimg_style()} />
|
||||
<Title show_sidebar={this.show_sidebar_bound} set_mode={this.set_mode_bound} />
|
||||
<TokenCtx.Consumer>{(token)=>(
|
||||
<div className="left-container">
|
||||
<DeprecatedAlert token={token.value} />
|
||||
{!token.value &&
|
||||
<div className="flow-item-row aux-margin">
|
||||
<div className="box box-tip">
|
||||
<p>
|
||||
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
|
||||
<a onClick={do_popup}>
|
||||
<span className="icon icon-login" />
|
||||
登录到 T大树洞
|
||||
</a>
|
||||
)}</LoginPopup>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{this.inthu_flag||token.value ?
|
||||
<Flow key={this.state.flow_render_key} show_sidebar={this.show_sidebar_bound}
|
||||
mode={this.state.mode} search_text={this.state.search_text} token={token.value}
|
||||
/> :
|
||||
<TitleLine text="请登录后查看内容" />
|
||||
}
|
||||
<br />
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
<Sidebar show_sidebar={this.show_sidebar_bound} stack={this.state.sidebar_stack} />
|
||||
</TokenCtx.Provider>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Provider
|
||||
value={{
|
||||
value: this.state.token,
|
||||
set_value: (x) => {
|
||||
localStorage['TOKEN'] = x || '';
|
||||
this.setState({
|
||||
token: x,
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PressureHelper callback={this.on_pressure_bound} />
|
||||
<div className="bg-img" style={bgimg_style()} />
|
||||
<Title
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
set_mode={this.set_mode_bound}
|
||||
/>
|
||||
<TokenCtx.Consumer>
|
||||
{(token) => (
|
||||
<div className="left-container">
|
||||
<DeprecatedAlert token={token.value} />
|
||||
{!token.value && (
|
||||
<div className="flow-item-row aux-margin">
|
||||
<div className="box box-tip">
|
||||
<p>
|
||||
<LoginPopup token_callback={token.set_value}>
|
||||
{(do_popup) => (
|
||||
<a onClick={do_popup}>
|
||||
<span className="icon icon-login" />
|
||||
登录到 T大树洞
|
||||
</a>
|
||||
)}
|
||||
</LoginPopup>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.inthu_flag || token.value ? (
|
||||
<Flow
|
||||
key={this.state.flow_render_key}
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
mode={this.state.mode}
|
||||
search_text={this.state.search_text}
|
||||
token={token.value}
|
||||
/>
|
||||
) : (
|
||||
<TitleLine text="请登录后查看内容" />
|
||||
)}
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
</TokenCtx.Consumer>
|
||||
<Sidebar
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
stack={this.state.sidebar_stack}
|
||||
/>
|
||||
</TokenCtx.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,91 +1,92 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import load from 'load-script';
|
||||
|
||||
window.audio_cache={};
|
||||
window.audio_cache = {};
|
||||
|
||||
function load_amrnb() {
|
||||
return new Promise((resolve,reject)=>{
|
||||
if(window.AMR)
|
||||
resolve();
|
||||
else
|
||||
load('static/amr_all.min.js', (err)=>{
|
||||
if(err)
|
||||
reject(err);
|
||||
else
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.AMR) resolve();
|
||||
else
|
||||
load('static/amr_all.min.js', (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class AudioWidget extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
url: this.props.src,
|
||||
state: 'waiting',
|
||||
data: null,
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
url: this.props.src,
|
||||
state: 'waiting',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
load() {
|
||||
if (window.audio_cache[this.state.url]) {
|
||||
this.setState({
|
||||
state: 'loaded',
|
||||
data: window.audio_cache[this.state.url],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
load() {
|
||||
if(window.audio_cache[this.state.url]) {
|
||||
this.setState({
|
||||
state: 'loaded',
|
||||
data: window.audio_cache[this.state.url],
|
||||
});
|
||||
console.log('fetching audio', this.state.url);
|
||||
this.setState({
|
||||
state: 'loading',
|
||||
});
|
||||
Promise.all([fetch(this.state.url), load_amrnb()]).then((res) => {
|
||||
res[0].blob().then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const raw = new window.AMR().decode(event.target.result);
|
||||
if (!raw) {
|
||||
alert('audio decoding failed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const wave = window.PCMData.encode({
|
||||
sampleRate: 8000,
|
||||
channelCount: 1,
|
||||
bytesPerSample: 2,
|
||||
data: raw,
|
||||
});
|
||||
const binary_wave = new Uint8Array(wave.length);
|
||||
for (let i = 0; i < wave.length; i++)
|
||||
binary_wave[i] = wave.charCodeAt(i);
|
||||
|
||||
console.log('fetching audio',this.state.url);
|
||||
this.setState({
|
||||
state: 'loading',
|
||||
});
|
||||
Promise.all([
|
||||
fetch(this.state.url),
|
||||
load_amrnb(),
|
||||
])
|
||||
.then((res)=>{
|
||||
res[0].blob().then((blob)=>{
|
||||
const reader=new FileReader();
|
||||
reader.onload=(event)=>{
|
||||
const raw=new window.AMR().decode(event.target.result);
|
||||
if(!raw) {
|
||||
alert('audio decoding failed');
|
||||
return;
|
||||
}
|
||||
const wave=window.PCMData.encode({
|
||||
sampleRate: 8000,
|
||||
channelCount: 1,
|
||||
bytesPerSample: 2,
|
||||
data: raw
|
||||
});
|
||||
const binary_wave=new Uint8Array(wave.length);
|
||||
for(let i=0;i<wave.length;i++)
|
||||
binary_wave[i]=wave.charCodeAt(i);
|
||||
const objurl = URL.createObjectURL(
|
||||
new Blob([binary_wave], { type: 'audio/wav' }),
|
||||
);
|
||||
window.audio_cache[this.state.url] = objurl;
|
||||
this.setState({
|
||||
state: 'loaded',
|
||||
data: objurl,
|
||||
});
|
||||
};
|
||||
reader.readAsBinaryString(blob);
|
||||
});
|
||||
this.setState({
|
||||
state: 'decoding',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const objurl=URL.createObjectURL(new Blob([binary_wave], {type: 'audio/wav'}));
|
||||
window.audio_cache[this.state.url]=objurl;
|
||||
this.setState({
|
||||
state: 'loaded',
|
||||
data: objurl,
|
||||
});
|
||||
};
|
||||
reader.readAsBinaryString(blob);
|
||||
});
|
||||
this.setState({
|
||||
state: 'decoding',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.state.state==='waiting')
|
||||
return (<p><a onClick={this.load.bind(this)}>加载音频</a></p>);
|
||||
if(this.state.state==='loading')
|
||||
return (<p>正在下载……</p>);
|
||||
else if(this.state.state==='decoding')
|
||||
return (<p>正在解码……</p>);
|
||||
else if(this.state.state==='loaded')
|
||||
return (<p><audio src={this.state.data} controls /></p>);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.state.state === 'waiting')
|
||||
return (
|
||||
<p>
|
||||
<a onClick={this.load.bind(this)}>加载音频</a>
|
||||
</p>
|
||||
);
|
||||
if (this.state.state === 'loading') return <p>正在下载……</p>;
|
||||
else if (this.state.state === 'decoding') return <p>正在解码……</p>;
|
||||
else if (this.state.state === 'loaded')
|
||||
return (
|
||||
<p>
|
||||
<audio src={this.state.data} controls />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
615
src/Common.js
615
src/Common.js
@@ -1,310 +1,415 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import {format_time,Time,TitleLine} from './infrastructure/widgets';
|
||||
import {THUHOLE_API_ROOT} from './flows_api';
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import { format_time, Time, TitleLine } from './infrastructure/widgets';
|
||||
import { THUHOLE_API_ROOT } from './flows_api';
|
||||
|
||||
import HtmlToReact from 'html-to-react'
|
||||
import HtmlToReact from 'html-to-react';
|
||||
|
||||
import './Common.css';
|
||||
import { URL_PID_RE, URL_RE, PID_RE, NICKNAME_RE, split_text } from './text_splitter';
|
||||
import {
|
||||
URL_PID_RE,
|
||||
URL_RE,
|
||||
PID_RE,
|
||||
NICKNAME_RE,
|
||||
split_text,
|
||||
} from './text_splitter';
|
||||
|
||||
import renderMd from './Markdown'
|
||||
import renderMd from './Markdown';
|
||||
|
||||
export {format_time,Time,TitleLine};
|
||||
export { format_time, Time, TitleLine };
|
||||
|
||||
export const API_BASE=THUHOLE_API_ROOT+'services/thuhole';
|
||||
export const API_BASE = THUHOLE_API_ROOT + 'services/thuhole';
|
||||
|
||||
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
||||
function escape_regex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function build_highlight_re(txt,split,option='g') {
|
||||
return txt ? new RegExp(`(${txt.split(split).filter((x)=>!!x).map(escape_regex).join('|')})`,option) : /^$/g;
|
||||
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;
|
||||
}
|
||||
|
||||
export function ColoredSpan(props) {
|
||||
return (
|
||||
<span className="colored-span" style={{
|
||||
'--coloredspan-bgcolor-light': props.colors[0],
|
||||
'--coloredspan-bgcolor-dark': props.colors[1],
|
||||
}}>{props.children}</span>
|
||||
)
|
||||
return (
|
||||
<span
|
||||
className="colored-span"
|
||||
style={{
|
||||
'--coloredspan-bgcolor-light': props.colors[0],
|
||||
'--coloredspan-bgcolor-dark': props.colors[1],
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function normalize_url(url) {
|
||||
return /^https?:\/\//.test(url) ? url : 'http://'+url;
|
||||
return /^https?:\/\//.test(url) ? url : 'http://' + url;
|
||||
}
|
||||
|
||||
export class HighlightedText extends PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<pre>
|
||||
{this.props.parts.map((part,idx)=>{
|
||||
let [rule,p]=part;
|
||||
return (
|
||||
<span key={idx}>{
|
||||
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> :
|
||||
rule==='url' ? <a href={normalize_url(p)} target="_blank" rel="noopener">{p}</a> :
|
||||
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); this.props.show_pid(p.substring(1));}}>{p}</a> :
|
||||
rule==='nickname' ? <ColoredSpan colors={this.props.color_picker.get(p)}>{p}</ColoredSpan> :
|
||||
rule==='search' ? <span className="search-query-highlight">{p}</span> :
|
||||
p
|
||||
}</span>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<pre>
|
||||
{this.props.parts.map((part, idx) => {
|
||||
let [rule, p] = part;
|
||||
return (
|
||||
<span key={idx}>
|
||||
{rule === 'url_pid' ? (
|
||||
<span className="url-pid-link" title={p}>
|
||||
/##
|
||||
</span>
|
||||
) : rule === 'url' ? (
|
||||
<a href={normalize_url(p)} target="_blank" rel="noopener">
|
||||
{p}
|
||||
</a>
|
||||
) : rule === 'pid' ? (
|
||||
<a
|
||||
href={'#' + p}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.props.show_pid(p.substring(1));
|
||||
}}
|
||||
>
|
||||
{p}
|
||||
</a>
|
||||
) : rule === 'nickname' ? (
|
||||
<ColoredSpan colors={this.props.color_picker.get(p)}>
|
||||
{p}
|
||||
</ColoredSpan>
|
||||
) : rule === 'search' ? (
|
||||
<span className="search-query-highlight">{p}</span>
|
||||
) : (
|
||||
p
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// props: text, show_pid, color_picker
|
||||
export class HighlightedMarkdown extends Component {
|
||||
render() {
|
||||
const props = this.props
|
||||
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React)
|
||||
const processInstructions = [
|
||||
{
|
||||
shouldProcessNode: (node) => node.name === 'img', // disable images
|
||||
processNode (node, children, index) {
|
||||
return (<div key={index}>[图片]</div>)
|
||||
}
|
||||
},
|
||||
{
|
||||
shouldProcessNode: (node) => (/^h[123456]$/.test(node.name)),
|
||||
processNode (node, children, index) {
|
||||
let currentLevel = +(node.name[1])
|
||||
if (currentLevel < 3) currentLevel = 3;
|
||||
const HeadingTag = `h${currentLevel}`
|
||||
return (
|
||||
<HeadingTag key={index}>{children}</HeadingTag>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
shouldProcessNode: (node) => node.name === 'a',
|
||||
processNode (node, children, index) {
|
||||
return (
|
||||
<a href={normalize_url(node.attribs.href)} target="_blank" rel="noopenner noreferrer" className="ext-link" key={index}>
|
||||
{children}
|
||||
<span className="icon icon-new-tab" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
shouldProcessNode (node) {
|
||||
return node.type === 'text' && (!node.parent || !node.parent.attribs || node.parent.attribs['encoding'] != "application/x-tex") // pid, nickname, search
|
||||
},
|
||||
processNode (node, children, index) {
|
||||
const originalText = node.data
|
||||
const splitted = split_text(originalText, [
|
||||
['url_pid', URL_PID_RE],
|
||||
['url',URL_RE],
|
||||
['pid',PID_RE],
|
||||
['nickname',NICKNAME_RE],
|
||||
])
|
||||
render() {
|
||||
const props = this.props;
|
||||
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React);
|
||||
const processInstructions = [
|
||||
{
|
||||
shouldProcessNode: (node) => node.name === 'img', // disable images
|
||||
processNode(node, children, index) {
|
||||
return <div key={index}>[图片]</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode: (node) => /^h[123456]$/.test(node.name),
|
||||
processNode(node, children, index) {
|
||||
let currentLevel = +node.name[1];
|
||||
if (currentLevel < 3) currentLevel = 3;
|
||||
const HeadingTag = `h${currentLevel}`;
|
||||
return <HeadingTag key={index}>{children}</HeadingTag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode: (node) => node.name === 'a',
|
||||
processNode(node, children, index) {
|
||||
return (
|
||||
<a
|
||||
href={normalize_url(node.attribs.href)}
|
||||
target="_blank"
|
||||
rel="noopenner noreferrer"
|
||||
className="ext-link"
|
||||
key={index}
|
||||
>
|
||||
{children}
|
||||
<span className="icon icon-new-tab" />
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode(node) {
|
||||
return (
|
||||
node.type === 'text' &&
|
||||
(!node.parent ||
|
||||
!node.parent.attribs ||
|
||||
node.parent.attribs['encoding'] != 'application/x-tex')
|
||||
); // pid, nickname, search
|
||||
},
|
||||
processNode(node, children, index) {
|
||||
const originalText = node.data;
|
||||
const splitted = split_text(originalText, [
|
||||
['url_pid', URL_PID_RE],
|
||||
['url', URL_RE],
|
||||
['pid', PID_RE],
|
||||
['nickname', NICKNAME_RE],
|
||||
]);
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{splitted.map(([rule, p], idx) => {
|
||||
return (<span key={idx}>
|
||||
{
|
||||
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> :
|
||||
rule==='url' ? <a href={normalize_url(p)} className="ext-link" target="_blank" rel="noopener noreferrer">
|
||||
{p}
|
||||
<span className="icon icon-new-tab" />
|
||||
</a> :
|
||||
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); props.show_pid(p.substring(1));}}>{p}</a> :
|
||||
rule==='nickname' ? <ColoredSpan colors={props.color_picker.get(p)}>{p}</ColoredSpan> :
|
||||
rule==='search' ? <span className="search-query-highlight">{p}</span> :
|
||||
p}
|
||||
</span>)
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
shouldProcessNode: () => true,
|
||||
processNode: processDefs.processDefaultNode
|
||||
}
|
||||
]
|
||||
const parser = new HtmlToReact.Parser()
|
||||
if (props.author && props.text.match(/^(?:#+ |>|```|\t|\s*-|\s*\d+\.)/)) {
|
||||
const renderedMarkdown = renderMd(props.text)
|
||||
return (
|
||||
<>
|
||||
{props.author}
|
||||
{parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || ''}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
let rawMd = props.text
|
||||
if (props.author) rawMd = props.author + ' ' + rawMd
|
||||
const renderedMarkdown = renderMd(rawMd)
|
||||
return (parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || null)
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{splitted.map(([rule, p], idx) => {
|
||||
return (
|
||||
<span key={idx}>
|
||||
{rule === 'url_pid' ? (
|
||||
<span className="url-pid-link" title={p}>
|
||||
/##
|
||||
</span>
|
||||
) : rule === 'url' ? (
|
||||
<a
|
||||
href={normalize_url(p)}
|
||||
className="ext-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{p}
|
||||
<span className="icon icon-new-tab" />
|
||||
</a>
|
||||
) : rule === 'pid' ? (
|
||||
<a
|
||||
href={'#' + p}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.show_pid(p.substring(1));
|
||||
}}
|
||||
>
|
||||
{p}
|
||||
</a>
|
||||
) : rule === 'nickname' ? (
|
||||
<ColoredSpan colors={props.color_picker.get(p)}>
|
||||
{p}
|
||||
</ColoredSpan>
|
||||
) : rule === 'search' ? (
|
||||
<span className="search-query-highlight">{p}</span>
|
||||
) : (
|
||||
p
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
shouldProcessNode: () => true,
|
||||
processNode: processDefs.processDefaultNode,
|
||||
},
|
||||
];
|
||||
const parser = new HtmlToReact.Parser();
|
||||
if (props.author && props.text.match(/^(?:#+ |>|```|\t|\s*-|\s*\d+\.)/)) {
|
||||
const renderedMarkdown = renderMd(props.text);
|
||||
return (
|
||||
<>
|
||||
{props.author}
|
||||
{parser.parseWithInstructions(
|
||||
renderedMarkdown,
|
||||
(node) => node.type !== 'script',
|
||||
processInstructions,
|
||||
) || ''}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let rawMd = props.text;
|
||||
if (props.author) rawMd = props.author + ' ' + rawMd;
|
||||
const renderedMarkdown = renderMd(rawMd);
|
||||
return (
|
||||
parser.parseWithInstructions(
|
||||
renderedMarkdown,
|
||||
(node) => node.type !== 'script',
|
||||
processInstructions,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.TEXTAREA_BACKUP={};
|
||||
window.TEXTAREA_BACKUP = {};
|
||||
|
||||
export class SafeTextarea extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
text: '',
|
||||
};
|
||||
this.on_change_bound=this.on_change.bind(this);
|
||||
this.on_keydown_bound=this.on_keydown.bind(this);
|
||||
this.clear=this.clear.bind(this);
|
||||
this.area_ref=React.createRef();
|
||||
this.change_callback=props.on_change||(()=>{});
|
||||
this.submit_callback=props.on_submit||(()=>{});
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
text: '',
|
||||
};
|
||||
this.on_change_bound = this.on_change.bind(this);
|
||||
this.on_keydown_bound = this.on_keydown.bind(this);
|
||||
this.clear = this.clear.bind(this);
|
||||
this.area_ref = React.createRef();
|
||||
this.change_callback = props.on_change || (() => {});
|
||||
this.submit_callback = props.on_submit || (() => {});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
text: window.TEXTAREA_BACKUP[this.props.id]||''
|
||||
},()=>{
|
||||
this.change_callback(this.state.text);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.TEXTAREA_BACKUP[this.props.id]=this.state.text;
|
||||
componentDidMount() {
|
||||
this.setState(
|
||||
{
|
||||
text: window.TEXTAREA_BACKUP[this.props.id] || '',
|
||||
},
|
||||
() => {
|
||||
this.change_callback(this.state.text);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
on_change(event) {
|
||||
this.setState({
|
||||
text: event.target.value,
|
||||
});
|
||||
this.change_callback(event.target.value);
|
||||
}
|
||||
on_keydown(event) {
|
||||
if(event.key==='Enter' && event.ctrlKey && !event.altKey) {
|
||||
event.preventDefault();
|
||||
this.submit_callback();
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.TEXTAREA_BACKUP[this.props.id] = this.state.text;
|
||||
this.change_callback(this.state.text);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setState({
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
set(text) {
|
||||
this.change_callback(text);
|
||||
this.setState({
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
get() {
|
||||
return this.state.text;
|
||||
}
|
||||
focus() {
|
||||
this.area_ref.current.focus();
|
||||
on_change(event) {
|
||||
this.setState({
|
||||
text: event.target.value,
|
||||
});
|
||||
this.change_callback(event.target.value);
|
||||
}
|
||||
on_keydown(event) {
|
||||
if (event.key === 'Enter' && event.ctrlKey && !event.altKey) {
|
||||
event.preventDefault();
|
||||
this.submit_callback();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<textarea ref={this.area_ref} onChange={this.on_change_bound} value={this.state.text} onKeyDown={this.on_keydown_bound} />
|
||||
)
|
||||
}
|
||||
clear() {
|
||||
this.setState({
|
||||
text: '',
|
||||
});
|
||||
}
|
||||
set(text) {
|
||||
this.change_callback(text);
|
||||
this.setState({
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
get() {
|
||||
return this.state.text;
|
||||
}
|
||||
focus() {
|
||||
this.area_ref.current.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<textarea
|
||||
ref={this.area_ref}
|
||||
onChange={this.on_change_bound}
|
||||
value={this.state.text}
|
||||
onKeyDown={this.on_keydown_bound}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let pwa_prompt_event=null;
|
||||
let pwa_prompt_event = null;
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
console.log('pwa: received before install prompt');
|
||||
pwa_prompt_event=e;
|
||||
console.log('pwa: received before install prompt');
|
||||
pwa_prompt_event = e;
|
||||
});
|
||||
|
||||
export function PromotionBar(props) {
|
||||
let is_ios=/iPhone|iPad|iPod/i.test(window.navigator.userAgent);
|
||||
let is_installed=(window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone);
|
||||
let is_ios = /iPhone|iPad|iPod/i.test(window.navigator.userAgent);
|
||||
let is_installed =
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
window.navigator.standalone;
|
||||
|
||||
if(is_installed)
|
||||
return null;
|
||||
if (is_installed) return null;
|
||||
|
||||
if(is_ios)
|
||||
// noinspection JSConstructorReturnsPrimitive
|
||||
return !navigator.standalone ? (
|
||||
<div className="box promotion-bar">
|
||||
<span className="icon icon-about" />
|
||||
用 Safari 把树洞 <b>添加到主屏幕</b> 更好用
|
||||
</div>
|
||||
) : null;
|
||||
else
|
||||
// noinspection JSConstructorReturnsPrimitive
|
||||
return pwa_prompt_event ? (
|
||||
<div className="box promotion-bar">
|
||||
<span className="icon icon-about" />
|
||||
把网页版树洞 <b><a onClick={()=>{
|
||||
if(pwa_prompt_event)
|
||||
pwa_prompt_event.prompt();
|
||||
}}>安装到桌面</a></b> 更好用
|
||||
</div>
|
||||
) : null;
|
||||
if (is_ios)
|
||||
// noinspection JSConstructorReturnsPrimitive
|
||||
return !navigator.standalone ? (
|
||||
<div className="box promotion-bar">
|
||||
<span className="icon icon-about" />
|
||||
用 Safari 把树洞 <b>添加到主屏幕</b> 更好用
|
||||
</div>
|
||||
) : null;
|
||||
// noinspection JSConstructorReturnsPrimitive
|
||||
else
|
||||
return pwa_prompt_event ? (
|
||||
<div className="box promotion-bar">
|
||||
<span className="icon icon-about" />
|
||||
把网页版树洞{' '}
|
||||
<b>
|
||||
<a
|
||||
onClick={() => {
|
||||
if (pwa_prompt_event) pwa_prompt_event.prompt();
|
||||
}}
|
||||
>
|
||||
安装到桌面
|
||||
</a>
|
||||
</b>{' '}
|
||||
更好用
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export class ClickHandler extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
moved: true,
|
||||
init_y: 0,
|
||||
init_x: 0,
|
||||
};
|
||||
this.on_begin_bound=this.on_begin.bind(this);
|
||||
this.on_move_bound=this.on_move.bind(this);
|
||||
this.on_end_bound=this.on_end.bind(this);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
moved: true,
|
||||
init_y: 0,
|
||||
init_x: 0,
|
||||
};
|
||||
this.on_begin_bound = this.on_begin.bind(this);
|
||||
this.on_move_bound = this.on_move.bind(this);
|
||||
this.on_end_bound = this.on_end.bind(this);
|
||||
|
||||
this.MOVE_THRESHOLD=3;
|
||||
this.last_fire=0;
|
||||
}
|
||||
this.MOVE_THRESHOLD = 3;
|
||||
this.last_fire = 0;
|
||||
}
|
||||
|
||||
on_begin(e) {
|
||||
//console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX);
|
||||
on_begin(e) {
|
||||
//console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX);
|
||||
this.setState({
|
||||
moved: false,
|
||||
init_y: (e.touches ? e.touches[0] : e).screenY,
|
||||
init_x: (e.touches ? e.touches[0] : e).screenX,
|
||||
});
|
||||
}
|
||||
on_move(e) {
|
||||
if (!this.state.moved) {
|
||||
let mvmt =
|
||||
Math.abs((e.touches ? e.touches[0] : e).screenY - this.state.init_y) +
|
||||
Math.abs((e.touches ? e.touches[0] : e).screenX - this.state.init_x);
|
||||
//console.log('move',mvmt);
|
||||
if (mvmt > this.MOVE_THRESHOLD)
|
||||
this.setState({
|
||||
moved: false,
|
||||
init_y: (e.touches?e.touches[0]:e).screenY,
|
||||
init_x: (e.touches?e.touches[0]:e).screenX,
|
||||
});
|
||||
}
|
||||
on_move(e) {
|
||||
if(!this.state.moved) {
|
||||
let mvmt=Math.abs((e.touches?e.touches[0]:e).screenY-this.state.init_y)+Math.abs((e.touches?e.touches[0]:e).screenX-this.state.init_x);
|
||||
//console.log('move',mvmt);
|
||||
if(mvmt>this.MOVE_THRESHOLD)
|
||||
this.setState({
|
||||
moved: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
on_end(event) {
|
||||
//console.log('end');
|
||||
if(!this.state.moved)
|
||||
this.do_callback(event);
|
||||
this.setState({
|
||||
moved: true,
|
||||
moved: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
on_end(event) {
|
||||
//console.log('end');
|
||||
if (!this.state.moved) this.do_callback(event);
|
||||
this.setState({
|
||||
moved: true,
|
||||
});
|
||||
}
|
||||
|
||||
do_callback(event) {
|
||||
if(this.last_fire+100>+new Date()) return;
|
||||
this.last_fire=+new Date();
|
||||
this.props.callback(event);
|
||||
}
|
||||
do_callback(event) {
|
||||
if (this.last_fire + 100 > +new Date()) return;
|
||||
this.last_fire = +new Date();
|
||||
this.props.callback(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div onTouchStart={this.on_begin_bound} onMouseDown={this.on_begin_bound}
|
||||
onTouchMove={this.on_move_bound} onMouseMove={this.on_move_bound}
|
||||
onClick={this.on_end_bound} >
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
onTouchStart={this.on_begin_bound}
|
||||
onMouseDown={this.on_begin_bound}
|
||||
onTouchMove={this.on_move_bound}
|
||||
onMouseMove={this.on_move_bound}
|
||||
onClick={this.on_end_bound}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
504
src/Config.js
504
src/Config.js
@@ -1,255 +1,327 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
|
||||
import './Config.css';
|
||||
|
||||
const BUILTIN_IMGS={
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg': '寻觅繁星(默认)',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/eriri.jpg': '平成著名画师',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/yurucamp.jpg': '露营天下第一',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/minecraft.jpg': '麦恩·库拉夫特',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/cyberpunk.jpg': '赛博城市',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/bj.jpg': '城市的星光',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/sif.jpg': '梦开始的地方',
|
||||
const BUILTIN_IMGS = {
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg':
|
||||
'寻觅繁星(默认)',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/eriri.jpg':
|
||||
'平成著名画师',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/yurucamp.jpg':
|
||||
'露营天下第一',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/minecraft.jpg':
|
||||
'麦恩·库拉夫特',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/cyberpunk.jpg':
|
||||
'赛博城市',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/bj.jpg':
|
||||
'城市的星光',
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/sif.jpg':
|
||||
'梦开始的地方',
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG={
|
||||
background_img: 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
|
||||
background_color: '#113366',
|
||||
pressure: false,
|
||||
easter_egg: true,
|
||||
color_scheme: 'default',
|
||||
fold: true
|
||||
const DEFAULT_CONFIG = {
|
||||
background_img:
|
||||
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
|
||||
background_color: '#113366',
|
||||
pressure: false,
|
||||
easter_egg: true,
|
||||
color_scheme: 'default',
|
||||
fold: true,
|
||||
};
|
||||
|
||||
export function load_config() {
|
||||
let config=Object.assign({},DEFAULT_CONFIG);
|
||||
let loaded_config;
|
||||
try {
|
||||
loaded_config=JSON.parse(localStorage['hole_config']||'{}');
|
||||
} catch(e) {
|
||||
alert('设置加载失败,将重置为默认设置!\n'+e);
|
||||
delete localStorage['hole_config'];
|
||||
loaded_config={};
|
||||
}
|
||||
let config = Object.assign({}, DEFAULT_CONFIG);
|
||||
let loaded_config;
|
||||
try {
|
||||
loaded_config = JSON.parse(localStorage['hole_config'] || '{}');
|
||||
} catch (e) {
|
||||
alert('设置加载失败,将重置为默认设置!\n' + e);
|
||||
delete localStorage['hole_config'];
|
||||
loaded_config = {};
|
||||
}
|
||||
|
||||
// unrecognized configs are removed
|
||||
Object.keys(loaded_config).forEach((key)=>{
|
||||
if(config[key]!==undefined)
|
||||
config[key]=loaded_config[key];
|
||||
});
|
||||
// unrecognized configs are removed
|
||||
Object.keys(loaded_config).forEach((key) => {
|
||||
if (config[key] !== undefined) config[key] = loaded_config[key];
|
||||
});
|
||||
|
||||
console.log('config loaded',config);
|
||||
window.config=config;
|
||||
console.log('config loaded', config);
|
||||
window.config = config;
|
||||
}
|
||||
export function save_config() {
|
||||
localStorage['hole_config']=JSON.stringify(window.config);
|
||||
load_config();
|
||||
localStorage['hole_config'] = JSON.stringify(window.config);
|
||||
load_config();
|
||||
}
|
||||
|
||||
export function bgimg_style(img,color) {
|
||||
if(img===undefined) img=window.config.background_img;
|
||||
if(color===undefined) color=window.config.background_color;
|
||||
return {
|
||||
background: 'transparent center center',
|
||||
backgroundImage: img===null ? 'unset' : 'url("'+encodeURI(img)+'")',
|
||||
backgroundColor: color,
|
||||
backgroundSize: 'cover',
|
||||
};
|
||||
export function bgimg_style(img, color) {
|
||||
if (img === undefined) img = window.config.background_img;
|
||||
if (color === undefined) color = window.config.background_color;
|
||||
return {
|
||||
background: 'transparent center center',
|
||||
backgroundImage: img === null ? 'unset' : 'url("' + encodeURI(img) + '")',
|
||||
backgroundColor: color,
|
||||
backgroundSize: 'cover',
|
||||
};
|
||||
}
|
||||
|
||||
class ConfigBackground extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
img: window.config.background_img,
|
||||
color: window.config.background_color,
|
||||
};
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
img: window.config.background_img,
|
||||
color: window.config.background_color,
|
||||
};
|
||||
}
|
||||
|
||||
save_changes() {
|
||||
this.props.callback({
|
||||
background_img: this.state.img,
|
||||
background_color: this.state.color,
|
||||
});
|
||||
}
|
||||
save_changes() {
|
||||
this.props.callback({
|
||||
background_img: this.state.img,
|
||||
background_color: this.state.color,
|
||||
});
|
||||
}
|
||||
|
||||
on_select(e) {
|
||||
let value=e.target.value;
|
||||
this.setState({
|
||||
img: value==='##other' ? '' :
|
||||
value==='##color' ? null : value,
|
||||
},this.save_changes.bind(this));
|
||||
}
|
||||
on_change_img(e) {
|
||||
this.setState({
|
||||
img: e.target.value,
|
||||
},this.save_changes.bind(this));
|
||||
}
|
||||
on_change_color(e) {
|
||||
this.setState({
|
||||
color: e.target.value,
|
||||
},this.save_changes.bind(this));
|
||||
}
|
||||
on_select(e) {
|
||||
let value = e.target.value;
|
||||
this.setState(
|
||||
{
|
||||
img: value === '##other' ? '' : value === '##color' ? null : value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
on_change_img(e) {
|
||||
this.setState(
|
||||
{
|
||||
img: e.target.value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
on_change_color(e) {
|
||||
this.setState(
|
||||
{
|
||||
color: e.target.value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let img_select= this.state.img===null ? '##color' :
|
||||
Object.keys(BUILTIN_IMGS).indexOf(this.state.img)===-1 ? '##other' : this.state.img;
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>背景图片:</b>
|
||||
<select value={img_select} onChange={this.on_select.bind(this)}>
|
||||
{Object.keys(BUILTIN_IMGS).map((key)=>(
|
||||
<option key={key} value={key}>{BUILTIN_IMGS[key]}</option>
|
||||
))}
|
||||
<option value="##other">输入图片网址……</option>
|
||||
<option value="##color">纯色背景……</option>
|
||||
</select>
|
||||
|
||||
{img_select==='##other' &&
|
||||
<input type="url" placeholder="图片网址" value={this.state.img} onChange={this.on_change_img.bind(this)} />
|
||||
}
|
||||
{img_select==='##color' &&
|
||||
<input type="color" value={this.state.color} onChange={this.on_change_color.bind(this)} />
|
||||
}
|
||||
</p>
|
||||
<div className="bg-preview" style={bgimg_style(this.state.img,this.state.color)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let img_select =
|
||||
this.state.img === null
|
||||
? '##color'
|
||||
: Object.keys(BUILTIN_IMGS).indexOf(this.state.img) === -1
|
||||
? '##other'
|
||||
: this.state.img;
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>背景图片:</b>
|
||||
<select value={img_select} onChange={this.on_select.bind(this)}>
|
||||
{Object.keys(BUILTIN_IMGS).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{BUILTIN_IMGS[key]}
|
||||
</option>
|
||||
))}
|
||||
<option value="##other">输入图片网址……</option>
|
||||
<option value="##color">纯色背景……</option>
|
||||
</select>
|
||||
|
||||
{img_select === '##other' && (
|
||||
<input
|
||||
type="url"
|
||||
placeholder="图片网址"
|
||||
value={this.state.img}
|
||||
onChange={this.on_change_img.bind(this)}
|
||||
/>
|
||||
)}
|
||||
{img_select === '##color' && (
|
||||
<input
|
||||
type="color"
|
||||
value={this.state.color}
|
||||
onChange={this.on_change_color.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<div
|
||||
className="bg-preview"
|
||||
style={bgimg_style(this.state.img, this.state.color)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigColorScheme extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
color_scheme: window.config.color_scheme,
|
||||
};
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
color_scheme: window.config.color_scheme,
|
||||
};
|
||||
}
|
||||
|
||||
save_changes() {
|
||||
this.props.callback({
|
||||
color_scheme: this.state.color_scheme,
|
||||
});
|
||||
}
|
||||
save_changes() {
|
||||
this.props.callback({
|
||||
color_scheme: this.state.color_scheme,
|
||||
});
|
||||
}
|
||||
|
||||
on_select(e) {
|
||||
let value=e.target.value;
|
||||
this.setState({
|
||||
color_scheme: value,
|
||||
},this.save_changes.bind(this));
|
||||
}
|
||||
on_select(e) {
|
||||
let value = e.target.value;
|
||||
this.setState(
|
||||
{
|
||||
color_scheme: value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>夜间模式:</b>
|
||||
<select value={this.state.color_scheme} onChange={this.on_select.bind(this)}>
|
||||
<option value="default">跟随系统</option>
|
||||
<option value="light">始终浅色模式</option>
|
||||
<option value="dark">始终深色模式</option>
|
||||
</select>
|
||||
<small>#color_scheme</small>
|
||||
</p>
|
||||
<p>
|
||||
选择浅色或深色模式,深色模式下将会调暗图片亮度
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<b>夜间模式:</b>
|
||||
<select
|
||||
value={this.state.color_scheme}
|
||||
onChange={this.on_select.bind(this)}
|
||||
>
|
||||
<option value="default">跟随系统</option>
|
||||
<option value="light">始终浅色模式</option>
|
||||
<option value="dark">始终深色模式</option>
|
||||
</select>
|
||||
<small>#color_scheme</small>
|
||||
</p>
|
||||
<p>选择浅色或深色模式,深色模式下将会调暗图片亮度</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigSwitch extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
switch: window.config[this.props.id],
|
||||
};
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
switch: window.config[this.props.id],
|
||||
};
|
||||
}
|
||||
|
||||
on_change(e) {
|
||||
let val=e.target.checked;
|
||||
this.setState({
|
||||
switch: val,
|
||||
},()=>{
|
||||
this.props.callback({
|
||||
[this.props.id]: val,
|
||||
});
|
||||
on_change(e) {
|
||||
let val = e.target.checked;
|
||||
this.setState(
|
||||
{
|
||||
switch: val,
|
||||
},
|
||||
() => {
|
||||
this.props.callback({
|
||||
[this.props.id]: val,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<label>
|
||||
<input name={'config-'+this.props.id} type="checkbox" checked={this.state.switch} onChange={this.on_change.bind(this)} />
|
||||
<b>{this.props.name}</b>
|
||||
<small>#{this.props.id}</small>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
{this.props.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
name={'config-' + this.props.id}
|
||||
type="checkbox"
|
||||
checked={this.state.switch}
|
||||
onChange={this.on_change.bind(this)}
|
||||
/>
|
||||
<b>{this.props.name}</b>
|
||||
<small>#{this.props.id}</small>
|
||||
</label>
|
||||
</p>
|
||||
<p>{this.props.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigUI extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.save_changes_bound=this.save_changes.bind(this);
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.save_changes_bound = this.save_changes.bind(this);
|
||||
}
|
||||
|
||||
save_changes(chg) {
|
||||
console.log(chg);
|
||||
Object.keys(chg).forEach((key)=>{
|
||||
window.config[key]=chg[key];
|
||||
});
|
||||
save_config();
|
||||
}
|
||||
save_changes(chg) {
|
||||
console.log(chg);
|
||||
Object.keys(chg).forEach((key) => {
|
||||
window.config[key] = chg[key];
|
||||
});
|
||||
save_config();
|
||||
}
|
||||
|
||||
reset_settings() {
|
||||
if(window.confirm('重置所有设置?')) {
|
||||
window.config={};
|
||||
save_config();
|
||||
window.location.reload();
|
||||
}
|
||||
reset_settings() {
|
||||
if (window.confirm('重置所有设置?')) {
|
||||
window.config = {};
|
||||
save_config();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="box config-ui-header">
|
||||
<p>这些功能仍在测试,可能不稳定(<a onClick={this.reset_settings.bind(this)}>全部重置</a>)</p>
|
||||
<p><b>修改设置后 <a onClick={()=>{window.location.reload()}}>刷新页面</a> 方可生效</b></p>
|
||||
</div>
|
||||
<div className="box">
|
||||
<ConfigBackground callback={this.save_changes_bound} />
|
||||
<hr />
|
||||
<ConfigColorScheme callback={this.save_changes_bound} />
|
||||
<hr />
|
||||
<ConfigSwitch callback={this.save_changes_bound} id="pressure" name="快速返回"
|
||||
description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞"
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch callback={this.save_changes_bound} id="easter_egg" name="允许彩蛋"
|
||||
description="在某些情况下显示彩蛋"
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch callback={this.save_changes_bound} id="fold" name="折叠树洞"
|
||||
description="在时间线中折叠可能引起不适的树洞"
|
||||
/>
|
||||
<hr />
|
||||
<p>
|
||||
新功能建议或问题反馈请在
|
||||
<a href="https://github.com/thuhole/thuhole-go-backend/issues" target="_blank">GitHub <span className="icon icon-github" /></a>
|
||||
提出。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="box config-ui-header">
|
||||
<p>
|
||||
这些功能仍在测试,可能不稳定(
|
||||
<a onClick={this.reset_settings.bind(this)}>全部重置</a>)
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
修改设置后{' '}
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
刷新页面
|
||||
</a>{' '}
|
||||
方可生效
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
<div className="box">
|
||||
<ConfigBackground callback={this.save_changes_bound} />
|
||||
<hr />
|
||||
<ConfigColorScheme callback={this.save_changes_bound} />
|
||||
<hr />
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
id="pressure"
|
||||
name="快速返回"
|
||||
description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞"
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
id="easter_egg"
|
||||
name="允许彩蛋"
|
||||
description="在某些情况下显示彩蛋"
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
id="fold"
|
||||
name="折叠树洞"
|
||||
description="在时间线中折叠可能引起不适的树洞"
|
||||
/>
|
||||
<hr />
|
||||
<p>
|
||||
新功能建议或问题反馈请在
|
||||
<a
|
||||
href="https://github.com/thuhole/thuhole-go-backend/issues"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub <span className="icon icon-github" />
|
||||
</a>
|
||||
提出。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1845
src/Flows.js
1845
src/Flows.js
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,33 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import MarkdownItKaTeX from 'markdown-it-katex'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import './Markdown.css'
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import MarkdownItKaTeX from 'markdown-it-katex';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/atom-one-dark.css';
|
||||
import './Markdown.css';
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
let md = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: false,
|
||||
breaks: true,
|
||||
inline: true,
|
||||
highlight (str, lang) {
|
||||
highlight(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs"><code>' +
|
||||
hljs.highlight(lang, str, true).value +
|
||||
'</code></pre>';
|
||||
return (
|
||||
'<pre class="hljs"><code>' +
|
||||
hljs.highlight(lang, str, true).value +
|
||||
'</code></pre>'
|
||||
);
|
||||
} catch (__) {}
|
||||
}
|
||||
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
|
||||
}
|
||||
return (
|
||||
'<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
|
||||
);
|
||||
},
|
||||
}).use(MarkdownItKaTeX, {
|
||||
"throwOnError" : false,
|
||||
"errorColor" : "#aa0000"
|
||||
})
|
||||
throwOnError: false,
|
||||
errorColor: '#aa0000',
|
||||
});
|
||||
|
||||
export default (text) => md.render(text)
|
||||
export default (text) => md.render(text);
|
||||
|
||||
135
src/Message.js
135
src/Message.js
@@ -1,65 +1,80 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import {THUHOLE_API_ROOT, get_json, API_VERSION_PARAM} from './flows_api';
|
||||
import {Time} from './Common';
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import { THUHOLE_API_ROOT, get_json, API_VERSION_PARAM } from './flows_api';
|
||||
import { Time } from './Common';
|
||||
|
||||
export class MessageViewer extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
loading_status: 'idle',
|
||||
msg: [],
|
||||
};
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading_status: 'idle',
|
||||
msg: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.load();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
if(this.state.loading_status==='loading') return;
|
||||
this.setState({
|
||||
loading_status: 'loading',
|
||||
},()=>{
|
||||
fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(this.props.token)+API_VERSION_PARAM())
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.error)
|
||||
throw new Error(json.error);
|
||||
else
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
msg: json.result,
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
alert(''+err);
|
||||
this.setState({
|
||||
loading_status: 'failed',
|
||||
});
|
||||
})
|
||||
load() {
|
||||
if (this.state.loading_status === 'loading') return;
|
||||
this.setState(
|
||||
{
|
||||
loading_status: 'loading',
|
||||
},
|
||||
() => {
|
||||
fetch(
|
||||
THUHOLE_API_ROOT +
|
||||
'api_xmcp/hole/system_msg?user_token=' +
|
||||
encodeURIComponent(this.props.token) +
|
||||
API_VERSION_PARAM(),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.error) throw new Error(json.error);
|
||||
else
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
msg: json.result,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
alert('' + err);
|
||||
this.setState({
|
||||
loading_status: 'failed',
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.state.loading_status==='loading')
|
||||
return (<p className="box box-tip">加载中……</p>);
|
||||
else if(this.state.loading_status==='failed')
|
||||
return (<div className="box box-tip"><a onClick={()=>{this.load()}}>重新加载</a></div>);
|
||||
else if(this.state.loading_status==='done')
|
||||
return this.state.msg.map((msg)=>(
|
||||
<div className="box">
|
||||
<div className="box-header">
|
||||
<Time stamp={msg.timestamp} short={false} />
|
||||
<b>{msg.title}</b>
|
||||
</div>
|
||||
<div className="box-content">
|
||||
<pre>{msg.content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.state.loading_status === 'loading')
|
||||
return <p className="box box-tip">加载中……</p>;
|
||||
else if (this.state.loading_status === 'failed')
|
||||
return (
|
||||
<div className="box box-tip">
|
||||
<a
|
||||
onClick={() => {
|
||||
this.load();
|
||||
}}
|
||||
>
|
||||
重新加载
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
else if (this.state.loading_status === 'done')
|
||||
return this.state.msg.map((msg) => (
|
||||
<div className="box">
|
||||
<div className="box-header">
|
||||
<Time stamp={msg.timestamp} short={false} />
|
||||
<b>{msg.title}</b>
|
||||
</div>
|
||||
<div className="box-content">
|
||||
<pre>{msg.content}</pre>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,120 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import Pressure from 'pressure';
|
||||
|
||||
import './PressureHelper.css';
|
||||
|
||||
const THRESHOLD=.4;
|
||||
const MULTIPLIER=25;
|
||||
const BORDER_WIDTH=500; // also change css!
|
||||
const THRESHOLD = 0.4;
|
||||
const MULTIPLIER = 25;
|
||||
const BORDER_WIDTH = 500; // also change css!
|
||||
|
||||
export class PressureHelper extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
level: 0,
|
||||
fired: false,
|
||||
};
|
||||
this.callback=props.callback;
|
||||
this.esc_interval=null;
|
||||
export class PressureHelper extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
level: 0,
|
||||
fired: false,
|
||||
};
|
||||
this.callback = props.callback;
|
||||
this.esc_interval = null;
|
||||
}
|
||||
|
||||
do_fire() {
|
||||
if (this.esc_interval) {
|
||||
clearInterval(this.esc_interval);
|
||||
this.esc_interval = null;
|
||||
}
|
||||
this.setState({
|
||||
level: 1,
|
||||
fired: true,
|
||||
});
|
||||
this.callback();
|
||||
window.setTimeout(() => {
|
||||
this.setState({
|
||||
level: 0,
|
||||
fired: false,
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
do_fire() {
|
||||
if(this.esc_interval) {
|
||||
clearInterval(this.esc_interval);
|
||||
this.esc_interval=null;
|
||||
}
|
||||
this.setState({
|
||||
level: 1,
|
||||
fired: true,
|
||||
});
|
||||
this.callback();
|
||||
window.setTimeout(()=>{
|
||||
componentDidMount() {
|
||||
if (window.config.pressure) {
|
||||
Pressure.set(
|
||||
document.body,
|
||||
{
|
||||
change: (force) => {
|
||||
if (!this.state.fired) {
|
||||
if (force >= 0.999) {
|
||||
this.do_fire();
|
||||
} else
|
||||
this.setState({
|
||||
level: force,
|
||||
});
|
||||
}
|
||||
},
|
||||
end: () => {
|
||||
this.setState({
|
||||
level: 0,
|
||||
fired: false,
|
||||
level: 0,
|
||||
fired: false,
|
||||
});
|
||||
},300);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
polyfill: false,
|
||||
only: 'touch',
|
||||
preventSelect: false,
|
||||
},
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
if(window.config.pressure) {
|
||||
Pressure.set(document.body, {
|
||||
change: (force)=>{
|
||||
if(!this.state.fired) {
|
||||
if(force>=.999) {
|
||||
this.do_fire();
|
||||
}
|
||||
else
|
||||
this.setState({
|
||||
level: force,
|
||||
});
|
||||
}
|
||||
},
|
||||
end: ()=>{
|
||||
this.setState({
|
||||
level: 0,
|
||||
fired: false,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
polyfill: false,
|
||||
only: 'touch',
|
||||
preventSelect: false,
|
||||
});
|
||||
|
||||
document.addEventListener('keydown',(e)=>{
|
||||
if(!e.repeat && e.key==='Escape') {
|
||||
if(this.esc_interval)
|
||||
clearInterval(this.esc_interval);
|
||||
this.setState({
|
||||
level: THRESHOLD/2,
|
||||
},()=>{
|
||||
this.esc_interval=setInterval(()=>{
|
||||
let new_level=this.state.level+.1;
|
||||
if(new_level>=.999)
|
||||
this.do_fire();
|
||||
else
|
||||
this.setState({
|
||||
level: new_level,
|
||||
});
|
||||
},30);
|
||||
});
|
||||
}
|
||||
});
|
||||
document.addEventListener('keyup',(e)=>{
|
||||
if(e.key==='Escape') {
|
||||
if(this.esc_interval) {
|
||||
clearInterval(this.esc_interval);
|
||||
this.esc_interval=null;
|
||||
}
|
||||
this.setState({
|
||||
level: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!e.repeat && e.key === 'Escape') {
|
||||
if (this.esc_interval) clearInterval(this.esc_interval);
|
||||
this.setState(
|
||||
{
|
||||
level: THRESHOLD / 2,
|
||||
},
|
||||
() => {
|
||||
this.esc_interval = setInterval(() => {
|
||||
let new_level = this.state.level + 0.1;
|
||||
if (new_level >= 0.999) this.do_fire();
|
||||
else
|
||||
this.setState({
|
||||
level: new_level,
|
||||
});
|
||||
}, 30);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (this.esc_interval) {
|
||||
clearInterval(this.esc_interval);
|
||||
this.esc_interval = null;
|
||||
}
|
||||
this.setState({
|
||||
level: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const pad=MULTIPLIER*(this.state.level-THRESHOLD)-BORDER_WIDTH;
|
||||
return (
|
||||
<div className={
|
||||
'pressure-box'
|
||||
+(this.state.fired ? ' pressure-box-fired' : '')
|
||||
+(this.state.level<=.0001 ? ' pressure-box-empty' : '')
|
||||
} style={{
|
||||
left: pad,
|
||||
right: pad,
|
||||
top: pad,
|
||||
bottom: pad,
|
||||
}} />
|
||||
)
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const pad = MULTIPLIER * (this.state.level - THRESHOLD) - BORDER_WIDTH;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'pressure-box' +
|
||||
(this.state.fired ? ' pressure-box-fired' : '') +
|
||||
(this.state.level <= 0.0001 ? ' pressure-box-empty' : '')
|
||||
}
|
||||
style={{
|
||||
left: pad,
|
||||
right: pad,
|
||||
top: pad,
|
||||
bottom: pad,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
101
src/Sidebar.js
101
src/Sidebar.js
@@ -1,45 +1,66 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import './Sidebar.css';
|
||||
|
||||
export class Sidebar extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.sidebar_ref=React.createRef();
|
||||
this.do_close_bound=this.do_close.bind(this);
|
||||
this.do_back_bound=this.do_back.bind(this);
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.sidebar_ref = React.createRef();
|
||||
this.do_close_bound = this.do_close.bind(this);
|
||||
this.do_back_bound = this.do_back.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps) {
|
||||
if(this.props.stack!==nextProps.stack) {
|
||||
//console.log('sidebar top');
|
||||
if(this.sidebar_ref.current)
|
||||
this.sidebar_ref.current.scrollTop=0;
|
||||
componentDidUpdate(nextProps) {
|
||||
if (this.props.stack !== nextProps.stack) {
|
||||
//console.log('sidebar top');
|
||||
if (this.sidebar_ref.current) this.sidebar_ref.current.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
do_close() {
|
||||
this.props.show_sidebar(null, null, 'clear');
|
||||
}
|
||||
do_back() {
|
||||
this.props.show_sidebar(null, null, 'pop');
|
||||
}
|
||||
|
||||
render() {
|
||||
let [cur_title, cur_content] = this.props.stack[
|
||||
this.props.stack.length - 1
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'sidebar-container ' +
|
||||
(cur_title !== null ? 'sidebar-on' : 'sidebar-off')
|
||||
}
|
||||
}
|
||||
|
||||
do_close() {
|
||||
this.props.show_sidebar(null,null,'clear');
|
||||
}
|
||||
do_back() {
|
||||
this.props.show_sidebar(null,null,'pop');
|
||||
}
|
||||
|
||||
render() {
|
||||
let [cur_title,cur_content]=this.props.stack[this.props.stack.length-1];
|
||||
return (
|
||||
<div className={'sidebar-container '+(cur_title!==null ? 'sidebar-on' : 'sidebar-off')}>
|
||||
<div className="sidebar-shadow" onClick={this.do_back_bound} onTouchEnd={(e)=>{e.preventDefault();e.target.click();}} />
|
||||
<div ref={this.sidebar_ref} className="sidebar">
|
||||
{cur_content}
|
||||
</div>
|
||||
<div className="sidebar-title">
|
||||
<a className="no-underline" onClick={this.do_close_bound}> <span className="icon icon-close" /> </a>
|
||||
{this.props.stack.length>2 &&
|
||||
<a className="no-underline" onClick={this.do_back_bound}> <span className="icon icon-back" /> </a>
|
||||
}
|
||||
{cur_title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="sidebar-shadow"
|
||||
onClick={this.do_back_bound}
|
||||
onTouchEnd={(e) => {
|
||||
e.preventDefault();
|
||||
e.target.click();
|
||||
}}
|
||||
/>
|
||||
<div ref={this.sidebar_ref} className="sidebar">
|
||||
{cur_content}
|
||||
</div>
|
||||
<div className="sidebar-title">
|
||||
<a className="no-underline" onClick={this.do_close_bound}>
|
||||
|
||||
<span className="icon icon-close" />
|
||||
|
||||
</a>
|
||||
{this.props.stack.length > 2 && (
|
||||
<a className="no-underline" onClick={this.do_back_bound}>
|
||||
|
||||
<span className="icon icon-back" />
|
||||
|
||||
</a>
|
||||
)}
|
||||
{cur_title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
291
src/Title.js
291
src/Title.js
@@ -1,143 +1,186 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
// import {AppSwitcher} from './infrastructure/widgets';
|
||||
import {InfoSidebar, PostForm} from './UserAction';
|
||||
import {TokenCtx} from './UserAction';
|
||||
import { InfoSidebar, PostForm } from './UserAction';
|
||||
import { TokenCtx } from './UserAction';
|
||||
|
||||
import './Title.css';
|
||||
|
||||
const flag_re=/^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/;
|
||||
const flag_re = /^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/;
|
||||
|
||||
class ControlBar extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
search_text: '',
|
||||
};
|
||||
this.set_mode=props.set_mode;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
search_text: '',
|
||||
};
|
||||
this.set_mode = props.set_mode;
|
||||
|
||||
this.on_change_bound=this.on_change.bind(this);
|
||||
this.on_keypress_bound=this.on_keypress.bind(this);
|
||||
this.do_refresh_bound=this.do_refresh.bind(this);
|
||||
this.do_attention_bound=this.do_attention.bind(this);
|
||||
this.on_change_bound = this.on_change.bind(this);
|
||||
this.on_keypress_bound = this.on_keypress.bind(this);
|
||||
this.do_refresh_bound = this.do_refresh.bind(this);
|
||||
this.do_attention_bound = this.do_attention.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (window.location.hash) {
|
||||
let text = decodeURIComponent(window.location.hash).substr(1);
|
||||
if (text.lastIndexOf('?') !== -1)
|
||||
text = text.substr(0, text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...'
|
||||
this.setState(
|
||||
{
|
||||
search_text: text,
|
||||
},
|
||||
() => {
|
||||
this.on_keypress({ key: 'Enter' });
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if(window.location.hash) {
|
||||
let text=decodeURIComponent(window.location.hash).substr(1);
|
||||
if(text.lastIndexOf('?')!==-1)
|
||||
text=text.substr(0,text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...'
|
||||
this.setState({
|
||||
search_text: text,
|
||||
}, ()=>{
|
||||
this.on_keypress({key: 'Enter'});
|
||||
});
|
||||
on_change(event) {
|
||||
this.setState({
|
||||
search_text: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
on_keypress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
let flag_res = flag_re.exec(this.state.search_text);
|
||||
if (flag_res) {
|
||||
if (flag_res[2]) {
|
||||
localStorage[flag_res[1]] = flag_res[2];
|
||||
alert(
|
||||
'Set Flag ' +
|
||||
flag_res[1] +
|
||||
'=' +
|
||||
flag_res[2] +
|
||||
'\nYou may need to refresh this webpage.',
|
||||
);
|
||||
} else {
|
||||
delete localStorage[flag_res[1]];
|
||||
alert(
|
||||
'Clear Flag ' +
|
||||
flag_res[1] +
|
||||
'\nYou may need to refresh this webpage.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
on_change(event) {
|
||||
this.setState({
|
||||
search_text: event.target.value,
|
||||
});
|
||||
const mode = this.state.search_text.startsWith('#') ? 'single' : 'search';
|
||||
this.set_mode(mode, this.state.search_text || '');
|
||||
}
|
||||
}
|
||||
|
||||
on_keypress(event) {
|
||||
if(event.key==='Enter') {
|
||||
let flag_res=flag_re.exec(this.state.search_text);
|
||||
if(flag_res) {
|
||||
if(flag_res[2]) {
|
||||
localStorage[flag_res[1]]=flag_res[2];
|
||||
alert('Set Flag '+flag_res[1]+'='+flag_res[2]+'\nYou may need to refresh this webpage.');
|
||||
} else {
|
||||
delete localStorage[flag_res[1]];
|
||||
alert('Clear Flag '+flag_res[1]+'\nYou may need to refresh this webpage.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
do_refresh() {
|
||||
window.scrollTo(0, 0);
|
||||
this.setState({
|
||||
search_text: '',
|
||||
});
|
||||
this.set_mode('list', null);
|
||||
}
|
||||
|
||||
const mode=this.state.search_text.startsWith('#') ? 'single' : 'search';
|
||||
this.set_mode(mode,this.state.search_text||'');
|
||||
}
|
||||
}
|
||||
do_attention() {
|
||||
window.scrollTo(0, 0);
|
||||
this.setState({
|
||||
search_text: '',
|
||||
});
|
||||
this.set_mode('attention', null);
|
||||
}
|
||||
|
||||
do_refresh() {
|
||||
window.scrollTo(0,0);
|
||||
this.setState({
|
||||
search_text: '',
|
||||
});
|
||||
this.set_mode('list',null);
|
||||
}
|
||||
|
||||
do_attention() {
|
||||
window.scrollTo(0,0);
|
||||
this.setState({
|
||||
search_text: '',
|
||||
});
|
||||
this.set_mode('attention',null);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>{({value: token})=>(
|
||||
<div className="control-bar">
|
||||
<a className="no-underline control-btn" onClick={this.do_refresh_bound}>
|
||||
<span className="icon icon-refresh" />
|
||||
<span className="control-btn-label">最新</span>
|
||||
</a>
|
||||
{!!token &&
|
||||
<a className="no-underline control-btn" onClick={this.do_attention_bound}>
|
||||
<span className="icon icon-attention" />
|
||||
<span className="control-btn-label">关注</span>
|
||||
</a>
|
||||
}
|
||||
<input className="control-search" value={this.state.search_text} placeholder="搜索 或 #树洞号"
|
||||
onChange={this.on_change_bound} onKeyPress={this.on_keypress_bound}
|
||||
/>
|
||||
<a className="no-underline control-btn" onClick={()=>{
|
||||
this.props.show_sidebar(
|
||||
'T大树洞',
|
||||
<InfoSidebar show_sidebar={this.props.show_sidebar} />
|
||||
)
|
||||
}}>
|
||||
<span className={'icon icon-'+(token ? 'about' : 'login')} />
|
||||
<span className="control-btn-label">{token ? '账户' : '登录'}</span>
|
||||
</a>
|
||||
{!!token &&
|
||||
<a className="no-underline control-btn" onClick={()=>{
|
||||
this.props.show_sidebar(
|
||||
'发表树洞',
|
||||
<PostForm token={token} on_complete={()=>{
|
||||
this.props.show_sidebar(null,null);
|
||||
this.do_refresh();
|
||||
}} />
|
||||
)
|
||||
}}>
|
||||
<span className="icon icon-plus" />
|
||||
<span className="control-btn-label">发表</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
)}</TokenCtx.Consumer>
|
||||
)
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<TokenCtx.Consumer>
|
||||
{({ value: token }) => (
|
||||
<div className="control-bar">
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={this.do_refresh_bound}
|
||||
>
|
||||
<span className="icon icon-refresh" />
|
||||
<span className="control-btn-label">最新</span>
|
||||
</a>
|
||||
{!!token && (
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={this.do_attention_bound}
|
||||
>
|
||||
<span className="icon icon-attention" />
|
||||
<span className="control-btn-label">关注</span>
|
||||
</a>
|
||||
)}
|
||||
<input
|
||||
className="control-search"
|
||||
value={this.state.search_text}
|
||||
placeholder="搜索 或 #树洞号"
|
||||
onChange={this.on_change_bound}
|
||||
onKeyPress={this.on_keypress_bound}
|
||||
/>
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={() => {
|
||||
this.props.show_sidebar(
|
||||
'T大树洞',
|
||||
<InfoSidebar show_sidebar={this.props.show_sidebar} />,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className={'icon icon-' + (token ? 'about' : 'login')} />
|
||||
<span className="control-btn-label">
|
||||
{token ? '账户' : '登录'}
|
||||
</span>
|
||||
</a>
|
||||
{!!token && (
|
||||
<a
|
||||
className="no-underline control-btn"
|
||||
onClick={() => {
|
||||
this.props.show_sidebar(
|
||||
'发表树洞',
|
||||
<PostForm
|
||||
token={token}
|
||||
on_complete={() => {
|
||||
this.props.show_sidebar(null, null);
|
||||
this.do_refresh();
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-plus" />
|
||||
<span className="control-btn-label">发表</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TokenCtx.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function Title(props) {
|
||||
return (
|
||||
<div className="title-bar">
|
||||
{/* <AppSwitcher appid="hole" /> */}
|
||||
<div className="aux-margin">
|
||||
<div className="title">
|
||||
<p className="centered-line">
|
||||
<span onClick={()=>props.show_sidebar(
|
||||
'T大树洞',
|
||||
<InfoSidebar show_sidebar={props.show_sidebar} />
|
||||
)}>
|
||||
T大树洞
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} />
|
||||
</div>
|
||||
return (
|
||||
<div className="title-bar">
|
||||
{/* <AppSwitcher appid="hole" /> */}
|
||||
<div className="aux-margin">
|
||||
<div className="title">
|
||||
<p className="centered-line">
|
||||
<span
|
||||
onClick={() =>
|
||||
props.show_sidebar(
|
||||
'T大树洞',
|
||||
<InfoSidebar show_sidebar={props.show_sidebar} />,
|
||||
)
|
||||
}
|
||||
>
|
||||
T大树洞
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ControlBar
|
||||
show_sidebar={props.show_sidebar}
|
||||
set_mode={props.set_mode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1223
src/UserAction.js
1223
src/UserAction.js
File diff suppressed because it is too large
Load Diff
319
src/cache.js
319
src/cache.js
@@ -1,172 +1,173 @@
|
||||
const HOLE_CACHE_DB_NAME='hole_cache_db';
|
||||
const CACHE_DB_VER=1;
|
||||
const MAINTENANCE_STEP=150;
|
||||
const MAINTENANCE_COUNT=1000;
|
||||
const HOLE_CACHE_DB_NAME = 'hole_cache_db';
|
||||
const CACHE_DB_VER = 1;
|
||||
const MAINTENANCE_STEP = 150;
|
||||
const MAINTENANCE_COUNT = 1000;
|
||||
|
||||
const ENC_KEY=42;
|
||||
const ENC_KEY = 42;
|
||||
|
||||
class Cache {
|
||||
constructor() {
|
||||
this.db=null;
|
||||
this.added_items_since_maintenance=0;
|
||||
this.encrypt=this.encrypt.bind(this);
|
||||
this.decrypt=this.decrypt.bind(this);
|
||||
const open_req=indexedDB.open(HOLE_CACHE_DB_NAME,CACHE_DB_VER);
|
||||
open_req.onerror=console.error.bind(console);
|
||||
open_req.onupgradeneeded=(event)=>{
|
||||
console.log('comment cache db upgrade');
|
||||
const db=event.target.result;
|
||||
const store=db.createObjectStore('comment',{
|
||||
keyPath: 'pid',
|
||||
});
|
||||
store.createIndex('last_access','last_access',{unique: false});
|
||||
};
|
||||
open_req.onsuccess=(event)=>{
|
||||
console.log('comment cache db loaded');
|
||||
this.db=event.target.result;
|
||||
setTimeout(this.maintenance.bind(this),1);
|
||||
};
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.added_items_since_maintenance = 0;
|
||||
this.encrypt = this.encrypt.bind(this);
|
||||
this.decrypt = this.decrypt.bind(this);
|
||||
const open_req = indexedDB.open(HOLE_CACHE_DB_NAME, CACHE_DB_VER);
|
||||
open_req.onerror = console.error.bind(console);
|
||||
open_req.onupgradeneeded = (event) => {
|
||||
console.log('comment cache db upgrade');
|
||||
const db = event.target.result;
|
||||
const store = db.createObjectStore('comment', {
|
||||
keyPath: 'pid',
|
||||
});
|
||||
store.createIndex('last_access', 'last_access', { unique: false });
|
||||
};
|
||||
open_req.onsuccess = (event) => {
|
||||
console.log('comment cache db loaded');
|
||||
this.db = event.target.result;
|
||||
setTimeout(this.maintenance.bind(this), 1);
|
||||
};
|
||||
}
|
||||
|
||||
// use window.hole_cache.encrypt() only after cache is loaded!
|
||||
encrypt(pid, data) {
|
||||
let s = JSON.stringify(data);
|
||||
let o = '';
|
||||
for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) {
|
||||
let c = s.charCodeAt(i);
|
||||
let new_key = (key ^ (c / 2)) % 128;
|
||||
o += String.fromCharCode(key ^ s.charCodeAt(i));
|
||||
key = new_key;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// use window.hole_cache.decrypt() only after cache is loaded!
|
||||
decrypt(pid, s) {
|
||||
let o = '';
|
||||
if (typeof s !== typeof 'str') return null;
|
||||
|
||||
for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) {
|
||||
let c = key ^ s.charCodeAt(i);
|
||||
o += String.fromCharCode(c);
|
||||
key = (key ^ (c / 2)) % 128;
|
||||
}
|
||||
|
||||
// use window.hole_cache.encrypt() only after cache is loaded!
|
||||
encrypt(pid,data) {
|
||||
let s=JSON.stringify(data);
|
||||
let o='';
|
||||
for(let i=0,key=(ENC_KEY^pid)%128;i<s.length;i++) {
|
||||
let c=s.charCodeAt(i);
|
||||
let new_key=(key^(c/2))%128;
|
||||
o+=String.fromCharCode(key^s.charCodeAt(i));
|
||||
key=new_key;
|
||||
try {
|
||||
return JSON.parse(o);
|
||||
} catch (e) {
|
||||
console.error('decrypt failed');
|
||||
console.trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get(pid, target_version) {
|
||||
pid = parseInt(pid);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.db) return resolve(null);
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
const get_req = store.get(pid);
|
||||
get_req.onsuccess = () => {
|
||||
let res = get_req.result;
|
||||
if (!res || !res.data_str) {
|
||||
//console.log('comment cache miss '+pid);
|
||||
resolve(null);
|
||||
} else if (target_version === res.version) {
|
||||
// hit
|
||||
console.log('comment cache hit', pid);
|
||||
res.last_access = +new Date();
|
||||
store.put(res);
|
||||
let data = this.decrypt(pid, res.data_str);
|
||||
resolve(data); // obj or null
|
||||
} else {
|
||||
// expired
|
||||
console.log(
|
||||
'comment cache expired',
|
||||
pid,
|
||||
': ver',
|
||||
res.version,
|
||||
'target',
|
||||
target_version,
|
||||
);
|
||||
store.delete(pid);
|
||||
resolve(null);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
};
|
||||
get_req.onerror = (e) => {
|
||||
console.warn('comment cache indexeddb open failed');
|
||||
console.error(e);
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// use window.hole_cache.decrypt() only after cache is loaded!
|
||||
decrypt(pid,s) {
|
||||
let o='';
|
||||
if(typeof(s)!==typeof('str'))
|
||||
return null;
|
||||
put(pid, target_version, data) {
|
||||
pid = parseInt(pid);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.db) return resolve();
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
store.put({
|
||||
pid: pid,
|
||||
version: target_version,
|
||||
data_str: this.encrypt(pid, data),
|
||||
last_access: +new Date(),
|
||||
});
|
||||
if (++this.added_items_since_maintenance === MAINTENANCE_STEP)
|
||||
setTimeout(this.maintenance.bind(this), 1);
|
||||
});
|
||||
}
|
||||
|
||||
for(let i=0,key=(ENC_KEY^pid)%128;i<s.length;i++) {
|
||||
let c=key^s.charCodeAt(i);
|
||||
o+=String.fromCharCode(c);
|
||||
key=(key^(c/2))%128;
|
||||
}
|
||||
delete(pid) {
|
||||
pid = parseInt(pid);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.db) return resolve();
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
let req = store.delete(pid);
|
||||
//console.log('comment cache delete',pid);
|
||||
req.onerror = () => {
|
||||
console.warn('comment cache delete failed ', pid);
|
||||
return resolve();
|
||||
};
|
||||
req.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(o);
|
||||
} catch(e) {
|
||||
console.error('decrypt failed');
|
||||
console.trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get(pid,target_version) {
|
||||
pid=parseInt(pid);
|
||||
return new Promise((resolve,reject)=>{
|
||||
if(!this.db)
|
||||
return resolve(null);
|
||||
const tx=this.db.transaction(['comment'],'readwrite');
|
||||
const store=tx.objectStore('comment');
|
||||
const get_req=store.get(pid);
|
||||
get_req.onsuccess=()=>{
|
||||
let res=get_req.result;
|
||||
if(!res || !res.data_str) {
|
||||
//console.log('comment cache miss '+pid);
|
||||
resolve(null);
|
||||
} else if(target_version===res.version) { // hit
|
||||
console.log('comment cache hit',pid);
|
||||
res.last_access=(+new Date());
|
||||
store.put(res);
|
||||
let data=this.decrypt(pid,res.data_str);
|
||||
resolve(data); // obj or null
|
||||
} else { // expired
|
||||
console.log('comment cache expired',pid,': ver',res.version,'target',target_version);
|
||||
store.delete(pid);
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
get_req.onerror=(e)=>{
|
||||
console.warn('comment cache indexeddb open failed');
|
||||
console.error(e);
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
put(pid,target_version,data) {
|
||||
pid=parseInt(pid);
|
||||
return new Promise((resolve,reject)=>{
|
||||
if(!this.db)
|
||||
return resolve();
|
||||
const tx=this.db.transaction(['comment'],'readwrite');
|
||||
const store=tx.objectStore('comment');
|
||||
store.put({
|
||||
pid: pid,
|
||||
version: target_version,
|
||||
data_str: this.encrypt(pid,data),
|
||||
last_access: +new Date(),
|
||||
});
|
||||
if(++this.added_items_since_maintenance===MAINTENANCE_STEP)
|
||||
setTimeout(this.maintenance.bind(this),1);
|
||||
});
|
||||
}
|
||||
|
||||
delete(pid) {
|
||||
pid=parseInt(pid);
|
||||
return new Promise((resolve,reject)=>{
|
||||
if(!this.db)
|
||||
return resolve();
|
||||
const tx=this.db.transaction(['comment'],'readwrite');
|
||||
const store=tx.objectStore('comment');
|
||||
let req=store.delete(pid);
|
||||
//console.log('comment cache delete',pid);
|
||||
req.onerror=()=>{
|
||||
console.warn('comment cache delete failed ',pid);
|
||||
return resolve();
|
||||
};
|
||||
req.onsuccess=()=>resolve();
|
||||
});
|
||||
}
|
||||
|
||||
maintenance() {
|
||||
if(!this.db)
|
||||
return;
|
||||
const tx=this.db.transaction(['comment'],'readwrite');
|
||||
const store=tx.objectStore('comment');
|
||||
let count_req=store.count();
|
||||
count_req.onsuccess=()=>{
|
||||
let count=count_req.result;
|
||||
if(count>MAINTENANCE_COUNT) {
|
||||
console.log('comment cache db maintenance',count);
|
||||
store.index('last_access').openKeyCursor().onsuccess=(e)=>{
|
||||
let cur=e.target.result;
|
||||
if(cur) {
|
||||
//console.log('maintenance: delete',cur);
|
||||
store.delete(cur.primaryKey);
|
||||
if(--count>MAINTENANCE_COUNT)
|
||||
cur.continue();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log('comment cache db no need to maintenance',count);
|
||||
}
|
||||
this.added_items_since_maintenance=0;
|
||||
maintenance() {
|
||||
if (!this.db) return;
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
let count_req = store.count();
|
||||
count_req.onsuccess = () => {
|
||||
let count = count_req.result;
|
||||
if (count > MAINTENANCE_COUNT) {
|
||||
console.log('comment cache db maintenance', count);
|
||||
store.index('last_access').openKeyCursor().onsuccess = (e) => {
|
||||
let cur = e.target.result;
|
||||
if (cur) {
|
||||
//console.log('maintenance: delete',cur);
|
||||
store.delete(cur.primaryKey);
|
||||
if (--count > MAINTENANCE_COUNT) cur.continue();
|
||||
}
|
||||
};
|
||||
count_req.onerror=console.error.bind(console);
|
||||
}
|
||||
} else {
|
||||
console.log('comment cache db no need to maintenance', count);
|
||||
}
|
||||
this.added_items_since_maintenance = 0;
|
||||
};
|
||||
count_req.onerror = console.error.bind(console);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if(!this.db)
|
||||
return;
|
||||
indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME);
|
||||
console.log('delete comment cache db');
|
||||
}
|
||||
};
|
||||
clear() {
|
||||
if (!this.db) return;
|
||||
indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME);
|
||||
console.log('delete comment cache db');
|
||||
}
|
||||
}
|
||||
|
||||
export function cache() {
|
||||
if(!window.hole_cache)
|
||||
window.hole_cache=new Cache();
|
||||
return window.hole_cache;
|
||||
}
|
||||
if (!window.hole_cache) window.hole_cache = new Cache();
|
||||
return window.hole_cache;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
// https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
|
||||
|
||||
const golden_ratio_conjugate=0.618033988749895;
|
||||
const golden_ratio_conjugate = 0.618033988749895;
|
||||
|
||||
export class ColorPicker {
|
||||
constructor() {
|
||||
this.names={};
|
||||
this.current_h=Math.random();
|
||||
}
|
||||
constructor() {
|
||||
this.names = {};
|
||||
this.current_h = Math.random();
|
||||
}
|
||||
|
||||
get(name) {
|
||||
name=name.toLowerCase();
|
||||
if(name==='洞主')
|
||||
return ['hsl(0,0%,97%)','hsl(0,0%,16%)'];
|
||||
get(name) {
|
||||
name = name.toLowerCase();
|
||||
if (name === '洞主') return ['hsl(0,0%,97%)', 'hsl(0,0%,16%)'];
|
||||
|
||||
if(!this.names[name]) {
|
||||
this.current_h+=golden_ratio_conjugate;
|
||||
this.current_h%=1;
|
||||
this.names[name]=[
|
||||
`hsl(${this.current_h*360}, 50%, 90%)`,
|
||||
`hsl(${this.current_h*360}, 60%, 20%)`,
|
||||
];
|
||||
}
|
||||
return this.names[name];
|
||||
if (!this.names[name]) {
|
||||
this.current_h += golden_ratio_conjugate;
|
||||
this.current_h %= 1;
|
||||
this.names[name] = [
|
||||
`hsl(${this.current_h * 360}, 50%, 90%)`,
|
||||
`hsl(${this.current_h * 360}, 60%, 20%)`,
|
||||
];
|
||||
}
|
||||
}
|
||||
return this.names[name];
|
||||
}
|
||||
}
|
||||
|
||||
329
src/flows_api.js
329
src/flows_api.js
@@ -1,183 +1,182 @@
|
||||
import {get_json, API_VERSION_PARAM} from './infrastructure/functions';
|
||||
import {THUHOLE_API_ROOT} from './infrastructure/const';
|
||||
import {API_BASE} from './Common';
|
||||
import {cache} from './cache';
|
||||
import { get_json, API_VERSION_PARAM } from './infrastructure/functions';
|
||||
import { THUHOLE_API_ROOT } from './infrastructure/const';
|
||||
import { API_BASE } from './Common';
|
||||
import { cache } from './cache';
|
||||
|
||||
export {THUHOLE_API_ROOT, API_VERSION_PARAM};
|
||||
export { THUHOLE_API_ROOT, API_VERSION_PARAM };
|
||||
|
||||
export function token_param(token) {
|
||||
return API_VERSION_PARAM()+(token ? ('&user_token='+token) : '');
|
||||
return API_VERSION_PARAM() + (token ? '&user_token=' + token : '');
|
||||
}
|
||||
|
||||
export {get_json};
|
||||
export { get_json };
|
||||
|
||||
const SEARCH_PAGESIZE=50;
|
||||
const SEARCH_PAGESIZE = 50;
|
||||
|
||||
export const API={
|
||||
load_replies: (pid,token,color_picker,cache_version)=>{
|
||||
pid=parseInt(pid);
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getcomment'+
|
||||
'&pid='+pid+
|
||||
token_param(token)
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.code!==0) {
|
||||
if(json.msg) throw new Error(json.msg);
|
||||
else throw new Error(JSON.stringify(json));
|
||||
}
|
||||
export const API = {
|
||||
load_replies: (pid, token, color_picker, cache_version) => {
|
||||
pid = parseInt(pid);
|
||||
return fetch(
|
||||
API_BASE +
|
||||
'/api.php?action=getcomment' +
|
||||
'&pid=' +
|
||||
pid +
|
||||
token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
if (json.msg) throw new Error(json.msg);
|
||||
else throw new Error(JSON.stringify(json));
|
||||
}
|
||||
|
||||
cache().delete(pid).then(()=>{
|
||||
cache().put(pid,cache_version,json);
|
||||
});
|
||||
cache()
|
||||
.delete(pid)
|
||||
.then(() => {
|
||||
cache().put(pid, cache_version, json);
|
||||
});
|
||||
|
||||
// also change load_replies_with_cache!
|
||||
json.data=json.data
|
||||
.sort((a,b)=>{
|
||||
return parseInt(a.cid,10)-parseInt(b.cid,10);
|
||||
})
|
||||
.map((info)=>{
|
||||
info._display_color=color_picker.get(info.name);
|
||||
info.variant={};
|
||||
return info;
|
||||
});
|
||||
// also change load_replies_with_cache!
|
||||
json.data = json.data
|
||||
.sort((a, b) => {
|
||||
return parseInt(a.cid, 10) - parseInt(b.cid, 10);
|
||||
})
|
||||
.map((info) => {
|
||||
info._display_color = color_picker.get(info.name);
|
||||
info.variant = {};
|
||||
return info;
|
||||
});
|
||||
|
||||
return json;
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
load_replies_with_cache: (pid, token, color_picker, cache_version) => {
|
||||
pid = parseInt(pid);
|
||||
return cache()
|
||||
.get(pid, cache_version)
|
||||
.then((json) => {
|
||||
if (json) {
|
||||
// also change load_replies!
|
||||
json.data = json.data
|
||||
.sort((a, b) => {
|
||||
return parseInt(a.cid, 10) - parseInt(b.cid, 10);
|
||||
})
|
||||
.map((info) => {
|
||||
info._display_color = color_picker.get(info.name);
|
||||
info.variant = {};
|
||||
return info;
|
||||
});
|
||||
},
|
||||
|
||||
load_replies_with_cache: (pid,token,color_picker,cache_version)=> {
|
||||
pid=parseInt(pid);
|
||||
return cache().get(pid,cache_version)
|
||||
.then((json)=>{
|
||||
if(json) {
|
||||
// also change load_replies!
|
||||
json.data=json.data
|
||||
.sort((a,b)=>{
|
||||
return parseInt(a.cid,10)-parseInt(b.cid,10);
|
||||
})
|
||||
.map((info)=>{
|
||||
info._display_color=color_picker.get(info.name);
|
||||
info.variant={};
|
||||
return info;
|
||||
});
|
||||
return json;
|
||||
} else return API.load_replies(pid, token, color_picker, cache_version);
|
||||
});
|
||||
},
|
||||
|
||||
return json;
|
||||
}
|
||||
else
|
||||
return API.load_replies(pid,token,color_picker,cache_version);
|
||||
});
|
||||
},
|
||||
set_attention: (pid, attention, token) => {
|
||||
let data = new URLSearchParams();
|
||||
data.append('user_token', token);
|
||||
data.append('pid', pid);
|
||||
data.append('switch', attention ? '1' : '0');
|
||||
return fetch(API_BASE + '/api.php?action=attention' + token_param(token), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
cache().delete(pid);
|
||||
if (json.code !== 0) {
|
||||
if (json.msg && json.msg === '已经关注过了') {
|
||||
} else {
|
||||
if (json.msg) alert(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
set_attention: (pid,attention,token)=>{
|
||||
let data=new URLSearchParams();
|
||||
data.append('user_token',token);
|
||||
data.append('pid',pid);
|
||||
data.append('switch',attention ? '1' : '0');
|
||||
return fetch(API_BASE+'/api.php?action=attention'+token_param(token), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
cache().delete(pid);
|
||||
if(json.code!==0) {
|
||||
if(json.msg && json.msg==='已经关注过了') {}
|
||||
else {
|
||||
if(json.msg) alert(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
report: (pid, reason, token) => {
|
||||
let data = new URLSearchParams();
|
||||
data.append('user_token', token);
|
||||
data.append('pid', pid);
|
||||
data.append('reason', reason);
|
||||
return fetch(API_BASE + '/api.php?action=report' + token_param(token), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
if (json.msg) alert(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
report: (pid,reason,token)=>{
|
||||
let data=new URLSearchParams();
|
||||
data.append('user_token',token);
|
||||
data.append('pid',pid);
|
||||
data.append('reason',reason);
|
||||
return fetch(API_BASE+'/api.php?action=report'+token_param(token), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
})
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.code!==0) {
|
||||
if(json.msg) alert(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
get_list: (page, token) => {
|
||||
return fetch(
|
||||
API_BASE + '/api.php?action=getlist' + '&p=' + page + token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) throw new Error(JSON.stringify(json));
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
get_list: (page,token)=>{
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getlist'+
|
||||
'&p='+page+
|
||||
token_param(token)
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.code!==0)
|
||||
throw new Error(JSON.stringify(json));
|
||||
return json;
|
||||
});
|
||||
},
|
||||
get_search: (page, keyword, token) => {
|
||||
return fetch(
|
||||
API_BASE +
|
||||
'/api.php?action=search' +
|
||||
'&pagesize=' +
|
||||
SEARCH_PAGESIZE +
|
||||
'&page=' +
|
||||
page +
|
||||
'&keywords=' +
|
||||
encodeURIComponent(keyword) +
|
||||
token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
if (json.msg) throw new Error(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
get_search: (page,keyword,token)=>{
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=search'+
|
||||
'&pagesize='+SEARCH_PAGESIZE+
|
||||
'&page='+page+
|
||||
'&keywords='+encodeURIComponent(keyword)+
|
||||
token_param(token)
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.code!==0) {
|
||||
if(json.msg) throw new Error(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
get_single: (pid, token) => {
|
||||
return fetch(
|
||||
API_BASE + '/api.php?action=getone' + '&pid=' + pid + token_param(token),
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
if (json.msg) throw new Error(json.msg);
|
||||
else throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
get_single: (pid,token)=>{
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getone'+
|
||||
'&pid='+pid+
|
||||
token_param(token)
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.code!==0) {
|
||||
if(json.msg) throw new Error(json.msg);
|
||||
else throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
|
||||
get_attention: (token)=>{
|
||||
return fetch(
|
||||
API_BASE+'/api.php?action=getattention'+
|
||||
token_param(token)
|
||||
)
|
||||
.then(get_json)
|
||||
.then((json)=>{
|
||||
if(json.code!==0) {
|
||||
if(json.msg) throw new Error(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
};
|
||||
get_attention: (token) => {
|
||||
return fetch(API_BASE + '/api.php?action=getattention' + token_param(token))
|
||||
.then(get_json)
|
||||
.then((json) => {
|
||||
if (json.code !== 0) {
|
||||
if (json.msg) throw new Error(json.msg);
|
||||
throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,8 +14,8 @@ const isLocalhost = Boolean(
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
@@ -23,10 +23,10 @@ export default function register() {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
// const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
// if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
// return;
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
// return;
|
||||
// }
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
@@ -41,7 +41,7 @@ export default function register() {
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -55,7 +55,7 @@ export default function register() {
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
@@ -76,7 +76,7 @@ function registerValidSW(swUrl) {
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
@@ -84,14 +84,14 @@ function registerValidSW(swUrl) {
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
@@ -103,14 +103,14 @@ function checkValidServiceWorker(swUrl) {
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
'No internet connection found. App is running in offline mode.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
// regexp should match the WHOLE segmented part
|
||||
// export const PID_RE=/(^|[^\d\u20e3\ufe0e\ufe0f])([2-9]\d{4,5}|1\d{4,6})(?![\d\u20e3\ufe0e\ufe0f])/g;
|
||||
export const PID_RE=/(^|[^\d\u20e3\ufe0e\ufe0f])(#\d{1,7})(?![\d\u20e3\ufe0e\ufe0f])/g;
|
||||
export const PID_RE = /(^|[^\d\u20e3\ufe0e\ufe0f])(#\d{1,7})(?![\d\u20e3\ufe0e\ufe0f])/g;
|
||||
// TODO: fix this re
|
||||
// export const URL_PID_RE=/((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)([2-9]\d{4,5}|1\d{4,6}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g;
|
||||
export const URL_PID_RE=/((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)(\d{1,7}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g;
|
||||
export const NICKNAME_RE=/(^|[^A-Za-z])((?:(?:Angry|Baby|Crazy|Diligent|Excited|Fat|Greedy|Hungry|Interesting|Jolly|Kind|Little|Magic|Naïve|Old|PKU|Quiet|Rich|Superman|Tough|Undefined|Valuable|Wifeless|Xiangbuchulai|Young|Zombie)\s)?(?:Alice|Bob|Carol|Dave|Eve|Francis|Grace|Hans|Isabella|Jason|Kate|Louis|Margaret|Nathan|Olivia|Paul|Queen|Richard|Susan|Thomas|Uma|Vivian|Winnie|Xander|Yasmine|Zach)|You Win(?: \d+)?|洞主)(?![A-Za-z])/gi;
|
||||
export const URL_RE=/(^|[^.@a-zA-Z0-9_])((?:https?:\/\/)?(?:(?:[\w-]+\.)+[a-zA-Z]{2,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/[\w~!@#$%^&*()\-_=+[\]{};:,./?|]*)?)(?![a-zA-Z0-9])/gi;
|
||||
export const URL_PID_RE = /((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)(\d{1,7}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g;
|
||||
export const NICKNAME_RE = /(^|[^A-Za-z])((?:(?:Angry|Baby|Crazy|Diligent|Excited|Fat|Greedy|Hungry|Interesting|Jolly|Kind|Little|Magic|Naïve|Old|PKU|Quiet|Rich|Superman|Tough|Undefined|Valuable|Wifeless|Xiangbuchulai|Young|Zombie)\s)?(?:Alice|Bob|Carol|Dave|Eve|Francis|Grace|Hans|Isabella|Jason|Kate|Louis|Margaret|Nathan|Olivia|Paul|Queen|Richard|Susan|Thomas|Uma|Vivian|Winnie|Xander|Yasmine|Zach)|You Win(?: \d+)?|洞主)(?![A-Za-z])/gi;
|
||||
export const URL_RE = /(^|[^.@a-zA-Z0-9_])((?:https?:\/\/)?(?:(?:[\w-]+\.)+[a-zA-Z]{2,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/[\w~!@#$%^&*()\-_=+[\]{};:,./?|]*)?)(?![a-zA-Z0-9])/gi;
|
||||
|
||||
export function split_text(txt,rules) {
|
||||
// rules: [['name',/regex/],...]
|
||||
// return: [['name','part'],[null,'part'],...]
|
||||
export function split_text(txt, rules) {
|
||||
// rules: [['name',/regex/],...]
|
||||
// return: [['name','part'],[null,'part'],...]
|
||||
|
||||
txt=[[null,txt]];
|
||||
rules.forEach((rule)=>{
|
||||
let [name,regex]=rule;
|
||||
txt=[].concat.apply([],txt.map((part)=>{
|
||||
let [rule,content]=part;
|
||||
if(rule) // already tagged by previous rules
|
||||
return [part];
|
||||
else {
|
||||
return content
|
||||
.split(regex)
|
||||
.map((seg)=>(
|
||||
regex.test(seg) ? [name,seg] : [null,seg]
|
||||
))
|
||||
.filter(([name,seg])=>(
|
||||
name!==null || seg
|
||||
));
|
||||
}
|
||||
}));
|
||||
});
|
||||
return txt;
|
||||
txt = [[null, txt]];
|
||||
rules.forEach((rule) => {
|
||||
let [name, regex] = rule;
|
||||
txt = [].concat.apply(
|
||||
[],
|
||||
txt.map((part) => {
|
||||
let [rule, content] = part;
|
||||
if (rule)
|
||||
// already tagged by previous rules
|
||||
return [part];
|
||||
else {
|
||||
return content
|
||||
.split(regex)
|
||||
.map((seg) => (regex.test(seg) ? [name, seg] : [null, seg]))
|
||||
.filter(([name, seg]) => name !== null || seg);
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
return txt;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user