
18 changed files with 3507 additions and 2668 deletions
@ -0,0 +1,55 @@
|
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es6": true, |
||||
"node": true |
||||
}, |
||||
"extends": [ |
||||
"plugin:react/recommended", |
||||
"plugin:prettier/recommended", |
||||
"prettier/react" |
||||
], |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly", |
||||
"React": true |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaFeatures": { |
||||
"jsx": true |
||||
}, |
||||
"ecmaVersion": 2018, |
||||
"sourceType": "module" |
||||
}, |
||||
"plugins": [ |
||||
"prettier", |
||||
"react" |
||||
], |
||||
"settings": { |
||||
"react": { |
||||
"version": "detect" |
||||
} |
||||
}, |
||||
"ignorePatterns": [ |
||||
"src/infrastructure/", |
||||
"src/react-lazyload/" |
||||
], |
||||
"rules": { |
||||
"prettier/prettier": "warn", |
||||
"react/jsx-indent": [ |
||||
"error", |
||||
2, |
||||
{ |
||||
"indentLogicalExpressions": true |
||||
} |
||||
], |
||||
"react/prop-types": "off", |
||||
"react/jsx-no-target-blank": "off", |
||||
"no-unused-vars": [ |
||||
"warn", |
||||
{ |
||||
"args": "none" |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,7 @@
|
||||
module.exports = { |
||||
trailingComma: 'all', |
||||
tabWidth: 2, |
||||
semi: true, |
||||
singleQuote: true, |
||||
endOfLine: 'auto', |
||||
} |
@ -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; |
||||
} |
||||
|
||||
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 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> |
||||
); |
||||
} |
||||
} |
||||
|
@ -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||(()=>{}); |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.setState({ |
||||
text: window.TEXTAREA_BACKUP[this.props.id]||'' |
||||
},()=>{ |
||||
this.change_callback(this.state.text); |
||||
}); |
||||
} |
||||
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 || (() => {}); |
||||
} |
||||
|
||||
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);
|
||||
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); |
||||
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: 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> |
||||
); |
||||
} |
||||
} |
||||
|
@ -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> |
||||
); |
||||
} |
||||
} |
||||
|
@ -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); |
||||
|
@ -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(() => { |
||||
this.setState({ |
||||
level: 0, |
||||
fired: false, |
||||
}); |
||||
}, 300); |
||||
} |
||||
|
||||
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, |
||||
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, |
||||
}); |
||||
}, |
||||
}, |
||||
{ |
||||
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, |
||||
}} |
||||
/> |
||||
); |
||||
} |
||||
} |
||||
|
@ -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'); |
||||
} |
||||
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> |
||||
); |
||||
} |
||||
} |
||||
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> |
||||
); |
||||
} |
||||
} |
||||
|
@ -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); |
||||
} |
||||
|
||||
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'}); |
||||
}); |
||||
} |
||||
} |
||||
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); |
||||
} |
||||
|
||||
on_change(event) { |
||||
this.setState({ |
||||
search_text: event.target.value, |
||||
}); |
||||
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_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); |
||||
} |
||||
|
||||
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> |
||||
) |
||||
} |
||||
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> |
||||
); |
||||
} |
||||
} |
||||
|
||||
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> |
||||
); |
||||
} |
||||
|
@ -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.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; |
||||
// 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; |
||||
} |
||||
|
||||
try { |
||||
return JSON.parse(o); |
||||
} catch(e) { |
||||
console.error('decrypt failed'); |
||||
console.trace(e); |
||||
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; |
||||
} |
||||
|
||||
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); |
||||
}; |
||||
}); |
||||
try { |
||||
return JSON.parse(o); |
||||
} catch (e) { |
||||
console.error('decrypt failed'); |
||||
console.trace(e); |
||||
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); |
||||
}); |
||||
} |
||||
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); |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
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(); |
||||
}); |
||||
} |
||||
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); |
||||
}); |
||||
} |
||||
|
||||
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; |
||||
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(); |
||||
} |
||||
}; |
||||
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]; |
||||
} |
||||
} |
||||
|
@ -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}; |
||||
|
||||
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)); |
||||
} |
||||
|
||||
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; |
||||
}); |
||||
|
||||
return json; |
||||
export { get_json }; |
||||
|
||||
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)); |
||||
} |
||||
|
||||
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; |
||||
}); |
||||
|
||||
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); |
||||
}); |
||||
}, |
||||
|
||||
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; |
||||
}); |
||||
}, |
||||
|
||||
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_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; |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
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; |
||||
}); |
||||
}, |
||||
|
||||
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_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_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; |
||||
}); |
||||
}, |
||||
}; |
||||
|
@ -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; |
||||
} |
||||
|
Loading…
Reference in new issue